Back to skill

Security audit

Ride Insights

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent local ride-receipt analysis workflow, but it handles sensitive Gmail, location, and payment-adjacent data that users should treat carefully.

Install only if you are comfortable granting the workflow access to ride-related Gmail receipts and storing raw receipt HTML locally. Use a private workspace, restrict file permissions, confirm your Gateway model is truly local before extraction, review generated ride records for poisoned or incorrect fields, and treat the exported CSV as de-identified rather than fully anonymous.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (3)

T01 · Skill Instruction Hijacking

Warning
Location
scripts/extract_rides_gateway.py:98
Finding

Untrusted Receipt Content Can Influence Model Instructions and Persisted Ride Records

Content
View full analysis

Vulnerability Details

File Location: scripts/extract_rides_gateway.py, lines 98-103
Vulnerability Type: Indirect prompt injection
Risk Level: Medium

Vulnerable Code:

python
def call_gateway(email_obj: dict, timeout: int = 180):
    base_url, token, model = load_gateway_settings()
    prompt = SYSTEM_PROMPT + "\n\nEmail JSON:\n" + json.dumps(email_obj, ensure_ascii=False)
    payload = {
        'model': model,
        'input': prompt,
    }

Related output handling in scripts/extract_rides_gateway.py, lines 124-143:

python
def normalize(result: dict, email_obj: dict):
    result = result or {}
    source = result.get('source') or {}
    ride = result.get('ride') or {}
    return {
        'provider': result.get('provider') or email_obj.get('provider') or 'Other',
        'source': {
            'gmail_message_id': source.get('gmail_message_id') or email_obj.get('gmail_message_id'),
            'email_date': source.get('email_date') or email_obj.get('email_date'),
            'subject': source.get('subject') or email_obj.get('subject'),
        },
        'ride': {k: ride.get(k) for k in [
            'start_time_text','end_time_text','total_text','currency','amount','pickup','dropoff',
            'pickup_city','pickup_country','dropoff_city','dropoff_country','payment_method','driver',
            'distance_text','duration_text','notes'
        ]},
    }


def write_results(out_path: Path, results: list[dict]):
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(results, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')

Technical Analysis

Receipt subjects, snippets, senders, and full HTML are attacker-influenced data. The extractor serializes the entire email object directly after the system instructions in a single model input. It does not explicitly tell the model that email content is untrusted ...[truncated 2227 chars]

Remediation
View remediation

Remediation Suggestions

  1. Clearly delimit receipt content as untrusted data and add an explicit instruction that text inside the receipt must never be interpreted as commands.
  2. Use separate system and user message roles, or structured input fields, if the Gateway API supports them.
  3. Validate responses against a strict JSON Schema before persistence.
  4. Enforce expected types, maximum lengths, and a fixed provider allowlist.
  5. Reject unexpected nested objects, unknown fields, control characters, and excessively long textual values.
  6. Preserve authoritative metadata such as Gmail message ID and provider exclusively from the fetched email object rather than accepting model-provided replacements.
  7. When generating insights, serialize stored fields as quoted untrusted data and repeat that no embedded instructions may be followed.
  8. Record validation failures and require review rather than silently persisting suspicious model output.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_emails_json.py:127
Finding

Sensitive Receipt and Location Data Is Stored Without Explicitly Restrictive File Permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/fetch_emails_json.py, lines 127-130
Vulnerability Type: Insecure plaintext storage and filesystem permissions
Risk Level: Medium

Vulnerable Code:

python
    items.sort(key=lambda x: ((x.get('email_date') or ''), (x.get('gmail_message_id') or '')))
    out_path = Path(args.out)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(items, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
    print(f'Wrote {len(items)} emails to {out_path}')

Equivalent ambient-permission behavior is also used when writing rides.json and creating the SQLite database.

Technical Analysis

The output contains Gmail message identifiers, sender and subject metadata, snippets, and potentially complete HTML receipts. Extracted artifacts additionally contain routes, payment methods, driver information, fares, and inferred location details.

The code creates directories and files using the process's ambient umask. It does not explicitly restrict directory permissions to 0700 or file and database permissions to 0600. On a system with a permissive umask or a shared workspace, other local users or processes may be able to read these artifacts.

The Skill documentation discloses that the records persist locally, but disclosure alone does not enforce access control. The storage is also unencrypted, so copies in backups or published workspaces remain readable.

Attack Path

  1. The Skill is run in a shared directory or under a permissive process umask.
  2. It creates emails.json, rides.json, or rides.sqlite using inherited filesystem permissions.
  3. The resulting artifact is readable by another local account, process, backup collector, synchronization client, or workspace publisher.
  4. That party reads raw receipt HTML or extracted financial and location history.
  5. The exposed information can be correlated to reconstru ...[truncated 632 chars]
Remediation
View remediation

Remediation Suggestions

  1. Create the data directory with mode 0700.
  2. Create JSON, CSV, and SQLite files with mode 0600, and correct permissions on existing files before reuse.
  3. Write sensitive files through a securely created temporary file, apply owner-only permissions, flush them, and atomically replace the destination.
  4. Refuse or prominently warn when the output directory is group-readable, world-readable, or located in a known shared workspace.
  5. Provide explicit retention and deletion commands for raw receipts and extracted records.
  6. Consider optional encryption at rest for emails.json, rides.json, and the SQLite database.
  7. Ensure temporary files, backups, and error logs do not contain raw receipt bodies or Gateway credentials.

other

Note
Location
scripts/export_anonymized_rides_csv.py:148
Finding

The Anonymized CSV Retains Combinations That May Re-identify Ride Activity

Content
View full analysis

Vulnerability Details

File Location: scripts/export_anonymized_rides_csv.py, lines 148-177
Vulnerability Type: Privacy re-identification risk
Risk Level: Low

Vulnerable Code:

python
    fieldnames = [
        "provider",
        "email_month",
        "start_time_15m",
        "end_time_15m",
        "currency",
        "amount",
        "distance_km",
        "duration_min",
        "pickup_city",
        "pickup_country",
        "dropoff_city",
        "dropoff_country",
    ]

    buffer = io.StringIO()
    writer = csv.DictWriter(buffer, fieldnames=fieldnames)
    writer.writeheader()
    for row in rows:
        distance_km, duration_min = extract_normalized_metrics(row["extracted_ride_json"])
        writer.writerow(
            {
                "provider": row["provider"] or "",
                "email_month": month_only(row["email_date_text"]),
                "start_time_15m": round_up_15(row["start_time_text"]),
                "end_time_15m": round_up_15(row["end_time_text"]),
                "currency": row["currency"] or "",
                "amount": row["amount"] if row["amount"] is not None else "",
                "distance_km": distance_km,
                "duration_min": duration_min,
                "pickup_city": row["pickup_city"] or "",
                "pickup_country": row["pickup_country"] or "",
                "dropoff_city": row["dropoff_city"] or "",
                "dropoff_country": row["dropoff_country"] or "",
            }
        )

Technical Analysis

The exporter correctly removes direct identifiers, street addresses, raw receipt content, payment methods, driver details, message IDs, and subjects. However, it retains provider, exact fare, distance, duration, city pair, month, and 15-minute time buckets.

A combination of these quasi-identifiers may be unique, particularly for uncommon intercity trips, airp ...[truncated 1314 chars]

Remediation
View remediation

Remediation Suggestions

  1. Describe the export as “de-identified” rather than fully anonymous.
  2. Require explicit confirmation immediately before attaching or sharing the generated CSV.
  3. Offer stronger privacy modes that use broad day-part buckets instead of 15-minute times.
  4. Round fares, distances, and durations to wider intervals.
  5. Allow country or region granularity instead of city-level locations.
  6. Suppress rare city pairs and rows that fail a minimum-group-size threshold.
  7. Provide a preview containing only column names and privacy settings before export.
  8. Preserve the existing warning that derived mobility data remains sensitive even after direct identifiers are removed.
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Tainted flow: 'req' from os.environ.get (line 104, credential/environment) → urllib.request.urlopen (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 · scripts/extract_rides_gateway.py (reported line 113)May include surrounding context.

python
},
        method='POST',
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        body = json.loads(resp.read().decode('utf-8'))
    for item in body.get('output', []):
        if item.get('type') == 'message':

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/fetch_emails_json.py (reported line 23)May include surrounding context.

python
last_err = None
    for attempt in range(1, retries + 1):
        try:
            out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)
            return json.loads(out)
        except subprocess.CalledProcessError as e:
            last_err = e

Static analysis

No suspicious patterns detected.