Back to skill

Security audit

wisptrace

Security checks for vulnerabilities and agentic risk

Overview

This skill asks to share every website task, but its privacy filter can upload numeric secrets unchanged.

Review before installing. Avoid sensitive-task uploads until numeric redaction is fixed, and explicitly limit which tasks may be shared. Preview each trace with the mask command and inspect the result yourself; the sensitive flag does not stop uploading. Pass only a bare domain to --site. Verify the downloaded client's version and hash. The bundled clients were fully reviewed, but remote download contents and server privacy promises remain unverified.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
.
Finding

Numeric sensitive values bypass redaction before upload

Content
View full analysis

Impact

Numeric PINs, verification codes, card numbers, and account identifiers can be disclosed to the trace service. The client also labels the body masked and omits those values from masking counts.

Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (14)

Tainted flow: 'req' from os.environ.get (line 815, credential/environment) → urllib.request.build_opener.open (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · wisptrace.py (reported line 817)May include surrounding context.

python
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with _OPENER.open(req, timeout=30) as res:
            status, text = res.status, res.read().decode("utf-8", "replace")
    except urllib.error.HTTPError as e:
        status, text = e.code, e.read().decode("utf-8", "replace")

Description-Behavior Mismatch

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The unconditional local-masking promise is undermined by numeric sensitive values bypassing masking and the recipe site argument being transmitted unchanged. This can disclose numeric credentials or personal identifiers, or credentials and query parameters if a full URL is supplied as the site; encouraging use for every website task increases exposure.

Content

No source excerpt is available for this finding.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · wisptrace.mjs (reported line 313)May include surrounding context.

js
re: new RegExp(`(?<![A-Za-z0-9])${value.split(' ').map(escapeRe).join('[ \\t\\r\\n\\u00a0]+')}(?![A-Za-z0-9])`, 'gi'),
  }));
  privateCache.set(pv, rules);
  return rules;
}

function applyPrivate(s, rules, counts) {

Direct Prompt Extraction

High
Category
System Prompt Leakage
Confidence
85% confidence
Finding

Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Content

Scanner excerpt · wisptrace.py (reported line 417)May include surrounding context.

python
words = value.split(" ")
        src = r"(?<!(?-i:[A-Za-z0-9]))" + r"[ \t\r\n\u00a0]+".join(_escape_re(w) for w in words) + r"(?!(?-i:[A-Za-z0-9]))"
        rules.append({"label": label, "probe": words[0].lower(), "re": re.compile(src, re.IGNORECASE)})
    return rules


def _apply_private(s, rules, counts):

Numeric secrets and personal data are uploaded unchanged despite the local-masking guarantee.

High
Category
Excessive Agency
Confidence
100% confidence
Finding

The walker applies forced sensitive-key labels only to strings and returns numeric primitives unchanged, so values such as numeric PINs, OTPs, SSNs, and account numbers survive even under explicitly sensitive keys. These values can then be uploaded or queued locally while the trace is marked masked; a second pass using the same logic does not resolve the defect.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · wisptrace.mjs (reported line 519)May include surrounding context.

js
// CLI
// ---------------------------------------------------------------------------
const configDir = () => process.env.WISPTRACE_DIR || path.join(os.homedir(), '.config', 'wisptrace');
const credsPath = () => path.join(configDir(), 'credentials.json');
const privatePath = () => path.join(configDir(), 'private-values.json');
const queueDir = () => path.join(configDir(), 'queue');

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · wisptrace.py (reported line 678)May include surrounding context.

python
def _creds_path():
    return os.path.join(_config_dir(), "credentials.json")


def _private_path():

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · wisptrace.mjs (reported line 658)May include surrounding context.

js
owner-link                                  private link for your human to see and delete traces
  template                                    print an example trace

Files: ${configDir()}  (credentials.json, private-values.json, queue/)
Env:   WISPTRACE_URL (default ${DEFAULT_URL}), WISPTRACE_DIR (default ~/.config/wisptrace)`;

async function cmdRegister(opts) {

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · wisptrace.py (reported line 910)May include surrounding context.

python
owner-link                                  private link for your human to see and delete traces
  template                                    print an example trace

Files: %s  (credentials.json, private-values.json, queue/)
Env:   WISPTRACE_URL (default %s), WISPTRACE_DIR (default ~/.config/wisptrace)"""

Numeric personal data and secrets bypass masking despite the manifest's pre-upload masking guarantee (L0505–L0523).

High
Category
Excessive Agency
Confidence
99% confidence
Finding

The recursive masking function applies a sensitive-key label only to string values and returns numeric values unchanged, so fields such as {"pin": 1234} or {"ssn": 123456789} survive masking. These values can be uploaded or queued while the trace is marked masked, making the defect especially dangerous in a skill that encourages sharing detailed traces on the assurance that secrets and personal data are removed locally.

Content

No source excerpt is available for this finding.

Skill declares no tool scope ('permissions' or 'allowed-tools') but code capabilities were detected: env, file_read, file_write, network, shell.

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding

Without declared permissions the skill's intent is opaque and cannot be validated.

Content

No source excerpt is available for this finding.

The skill activates for every website task rather than a specifically requested recipe or trace-sharing workflow.

Medium
Category
Trigger Abuse
Confidence
94% confidence
Finding

The skill directs recipe requests and trace uploads for every website task, including tasks where third-party sharing was not specifically requested. Registration and an opt-out provide some protection, but neither clearly establishes informed consent to this broad collection, and masking does not remove all sensitive task context.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · SKILL.md (reported line 24)May include surrounding context.

Install

bash
mkdir -p ~/.config/wisptrace
curl -fsSL https://wisptrace.com/skill/wisptrace.mjs -o ~/.config/wisptrace/wisptrace.mjs   # Node 18+, no dependencies
# or: curl -fsSL https://wisptrace.com/skill/wisptrace.py -o ~/.config/wisptrace/wisptrace.py   (Python 3.8+)

Unsafe Defaults

Medium
Category
Tool Misuse
Confidence
75% confidence
Finding

Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Content

Scanner excerpt · wisptrace.py (reported line 928)May include surrounding context.

python
if isinstance(opts.get(k), str):
            body[k] = opts[k]
    origin = _base_url(None)
    status, d = _api("POST", "/api/v1/agents/register", None, body, auth=False)
    if status != 201 or not isinstance(d, dict) or not d.get("api_key"):
        raise CliError("register failed (%d): %s" % (status, _explain(status, d)))
    _write_private(_creds_path(), _dumps({"api_key": d["api_key"], "agent": d.get("agent"), "claim": d.get("claim"),