Back to skill

Security audit

Fastmail JMAP

Security checks for vulnerabilities and agentic risk

Overview

This skill is a straightforward Fastmail email CLI that discloses its broad mailbox access, but users should treat it as sensitive because it can read, send, and modify email.

Install only for a Fastmail account or mailbox you are comfortable letting an agent read and modify. Use the narrowest Fastmail token practical, require explicit approval before sending or changing messages, avoid unattended cron monitoring unless you have a clear need, and be cautious with terminal output from untrusted email senders.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fastmail.py:164
Finding
Unsanitized Email Content Enables Terminal Output Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fastmail.py`, lines 164–187 and 316–322 **Vulnerability Type**: Terminal control-sequence injection through untrusted email content **Risk Level**: Medium ### Vulnerable Code ```python print(f" Subject: {e['subject']}") print(f" From: {fr_name} <{fr}>") print(f" To: {to}") if cc: print(f" CC: {cc}") print(f" Date: {e['receivedAt']}") print(f" Status: {'UNREAD' if unread else 'read'}") print(f" {'─' * 60}") body_text = "" for part in e.get("textBody", []): val = e.get("bodyValues", {}).get(part["partId"], {}) if val.get("value"): body_text += val["value"] if not body_text: for part in e.get("htmlBody", []): val = e.get("bodyValues", {}).get(part["partId"], {}) if val.get("value"): body_text += val["value"] print(body_text[:5000] if body_text else " (no body)") ``` Additional affected output path: ```python subj = e.get("subject", "(no subject)") print(f" {flag} {dt} | {fr:30s} | {subj}") print(f" {e['preview'][:120]}") print(f" id: {e['id']}") print() ``` ### Technical Analysis Email subjects, sender names, sender addresses, previews, and message bodies are controlled by external email senders. The script prints these values directly to the terminal without removing or escaping terminal control characters. A malicious message can contain ANSI or OSC escape sequences. Depending on the terminal emulator, these sequences can: - Change colors or overwrite visible terminal content. - Hide or forge status messages and command output. - Render deceptive terminal hyperlinks. - Change terminal titles. - Attempt clipboard manipulation through supported OSC sequences. - Mislead a human or an AI agent that consumes captured terminal output. Truncating the body by character count does not mitigate the issue because a functional escape sequence can fit within a few bytes. This flaw does not, by itself, execute shell ...[truncated 1321 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Sanitize every untrusted field before printing it in human-readable mode, including subject, sender name, sender address, recipient fields, preview, and body. 2. Remove or visibly escape C0 and C1 control characters, particularly `ESC` (`\x1b`), while selectively preserving safe formatting characters such as newline and tab. 3. Apply sanitization after truncation as well as before display, ensuring that truncation cannot leave malformed control sequences. 4. Use a dedicated helper consistently: ```python import re _CONTROL_CHARS = re.compile( r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]" ) def safe_terminal_text(value): if value is None: return "" return _CONTROL_CHARS.sub( lambda match: f"\\x{ord(match.group()):02x}", str(value), ) ``` 5. Pass every remotely sourced value through `safe_terminal_text()` before interpolation. 6. Prefer JSON output for agent-to-agent processing. Consumers should parse JSON rather than treating human-readable terminal output as trusted instructions. 7. Add tests covering ANSI color sequences, cursor movement, OSC hyperlinks, OSC clipboard sequences, carriage returns, backspaces, and embedded null bytes. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/fastmail.py:73
Finding
Configured Sender Identity Causes Send Command Failure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fastmail.py`, lines 73–84 **Vulnerability Type**: Inconsistent return type in sender identity resolution **Risk Level**: Low ### Vulnerable Code ```python def _get_identity(): """Get the first identity (sender address) for the account.""" override = os.environ.get("FASTMAIL_IDENTITY") if override: return override resp = _call([["Identity/get", {"accountId": None}, "0"]], using=["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"]) identities = resp["methodResponses"][0][1]["list"] if not identities: print("Error: No sending identities found.", file=sys.stderr) sys.exit(1) return identities[0]["id"], identities[0]["email"] ``` The result is later consumed as a two-element tuple: ```python identity_id, identity_email = _get_identity() ``` ### Technical Analysis When `FASTMAIL_IDENTITY` is absent, `_get_identity()` returns an `(identity_id, identity_email)` tuple. When the documented override is present, it returns a single string. The caller always unpacks the return value into two variables. Most email-address strings therefore cause a `ValueError` because they contain more than two characters. An unusual two-character string would unpack into individual characters, but it would not represent a valid resolved Fastmail identity. The override also supplies only an email address, whereas `EmailSubmission/set` requires an authorized JMAP identity ID. The implementation must resolve the configured address through `Identity/get`; treating the address as a complete identity result is insufficient. ### Attack Path 1. A user follows the documentation and sets `FASTMAIL_IDENTITY` to a sender email address. 2. The user or agent invokes the `send` command. 3. `_get_identity()` returns the configured string rather than a tuple. 4. `cmd_send()` attempts to unpack the string into `identity_id` and `identity_email`. 5. Python ra ...[truncated 772 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make `_get_identity()` return the same tuple shape on every successful path. 2. Retrieve the authorized identities with `Identity/get`. 3. If `FASTMAIL_IDENTITY` is configured, compare it against the returned identity email addresses and return the matching identity ID and address. 4. Reject unknown or unauthorized configured addresses with a clear error. 5. Validate the explicit `--from` value against the selected authorized identity rather than allowing an arbitrary sender address to proceed to an avoidable API failure. 6. Add tests for an unset override, a valid override, an unknown override, no available identities, and multiple identities. Example approach: ```python def _get_identity(): override = os.environ.get("FASTMAIL_IDENTITY") resp = _call( [["Identity/get", {"accountId": None}, "0"]], using=[ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission", ], ) identities = resp["methodResponses"][0][1]["list"] if not identities: print("Error: No sending identities found.", file=sys.stderr) sys.exit(1) if override: for identity in identities: if identity.get("email", "").lower() == override.lower(): return identity["id"], identity["email"] print( f"Error: FASTMAIL_IDENTITY is not an authorized identity: {override}", file=sys.stderr, ) sys.exit(1) return identities[0]["id"], identities[0]["email"] ``` ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (10)

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

Critical
Category
Data Flow
Content
return
    headers = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
    req = urllib.request.Request("https://api.fastmail.com/jmap/session", headers=headers)
    session = json.loads(urllib.request.urlopen(req).read())
    ACCOUNT = list(session["accounts"].keys())[0]
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
headers = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
    body = json.dumps({"using": using or USING, "methodCalls": method_calls}).encode()
    req = urllib.request.Request(API, body, headers, method="POST")
    resp = json.loads(urllib.request.urlopen(req).read())
    # Check for errors
    for mr in resp.get("methodResponses", []):
        if mr[0] == "error":
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Credential Access

High
Category
Privilege Escalation
Content
}
```

Or use 1Password injection: `op run --env-file=.env -- python3 scripts/fastmail.py unread`

## How It Works
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Ae1

High
Category
analysis-evasion
Content
- `SKILL.md` — this file
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill advertises capabilities that require environment-variable access and outbound network access, but it does not declare any explicit tool scope or permissions boundary. That weakens least-privilege controls and makes it easier for an agent runtime to grant broader access than users may expect, especially for a skill that can read and send email.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The introductory description encourages agents to read and manage email but does not immediately warn that mailbox contents are highly sensitive personal and business data. Users may enable the skill without understanding that it grants broad access to potentially confidential communications and attachments.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
### Sending email
- `python3 scripts/fastmail.py send "user@example.com" "Subject" "Body text"`
- Always ask before sending. Never send without approval.
```

### In heartbeat/cron:
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The heartbeat/cron guidance recommends unattended inbox monitoring and summarization without a clear warning about continuous surveillance of sensitive email. Persistent automated access increases the chance of overcollection, privacy violations, and unnoticed processing of confidential or regulated content.

External Transmission

Medium
Category
Data Exfiltration
Content
print("Get one at: https://app.fastmail.com/settings/security/tokens", file=sys.stderr)
    sys.exit(1)

API = "https://api.fastmail.com/jmap/api/"
USING = ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"]
ACCOUNT = None
IDENTITY = None
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
print("Get one at: https://app.fastmail.com/settings/security/tokens", file=sys.stderr)
    sys.exit(1)

API = "https://api.fastmail.com/jmap/api/"
USING = ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"]
ACCOUNT = None
IDENTITY = None
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.