Back to skill

Security audit

Gmail Link Archiver

Security checks for vulnerabilities and agentic risk

Overview

This skill is a real Gmail link archiver, but it stores a reusable Gmail app password locally and automatically crawls email-supplied URLs with too little containment.

Install only if you are comfortable granting this skill persistent Gmail IMAP access and letting it browse links found in matching emails from your machine. Use a dedicated Gmail app password, revoke it when finished, review the saved config file, keep the subject prefix hard to guess, and avoid running setup or Playwright dependency commands with administrator privileges. A safer version would use an OS keychain, pinned dependencies, URL review, and blocking of localhost, private networks, cloud metadata addresses, and unsafe redirects.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
references/gmail_link_archiver.py:162
Finding
Attacker-Controlled Email Links Can Access Internal Network Resources<![CDATA[ ## Vulnerability Details **File Location**: `references/gmail_link_archiver.py:162-191`, `references/gmail_link_archiver.py:247-249`, and `references/gmail_link_archiver.py:365-380` **Vulnerability Type**: Server-Side Request Forgery through unrestricted browser navigation **Risk Level**: High ### Vulnerable Code ```python def extract_links_from_email(msg) -> list: """Extract HTTP/HTTPS links from email body (HTML and plain text parts).""" links = set() url_pattern = re.compile(r'https?://[^\s<>"\')\]]+(?<![.,;:])') if msg.is_multipart(): for part in msg.walk(): ctype = part.get_content_type() try: payload = part.get_payload(decode=True) if payload is None: continue text = payload.decode("utf-8", errors="replace") except Exception: continue if ctype in ("text/plain", "text/html"): found = url_pattern.findall(text) links.update(found) else: try: payload = msg.get_payload(decode=True) if payload: text = payload.decode("utf-8", errors="replace") links.update(url_pattern.findall(text)) except Exception: pass # Filter out common tracking / unsubscribe links filtered = [ l for l in links if not any(skip in l.lower() for skip in [ "unsubscribe", "tracking", "click.email", "list-manage", "mailchimp", "googleadservices", ]) ] return sorted(filtered) ``` ```python page = context.new_page() page.goto(url, wait_until="networkidle", timeout=timeout) # Wait a bit for JS-rendered content page.wait_for_timeout(2000) return page.content() ``` ```python for em in emails: for link in em["links"]: if link not in link_subjects: link_subjects[link] = em["subject"] all_links.append(link) print(f"\n ...[truncated 3041 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse every URL with a standards-compliant URL parser and permit only explicitly supported schemes, normally HTTPS. 2. Resolve the hostname before navigation and reject every resolved IPv4 or IPv6 address that is loopback, private, link-local, multicast, reserved, unspecified, or otherwise non-global. 3. Explicitly block known metadata destinations, including `169.254.169.254`, relevant IPv6 link-local addresses, and provider-specific metadata hostnames. 4. Restrict destination ports to a narrow allowlist, such as 443 and, only if necessary, 80. 5. Validate every redirect destination using the same policy. Do not rely solely on checking the initial URL. 6. Defend against DNS rebinding by binding validation to the actual connection destination or by routing traffic through a hardened outbound proxy that enforces destination policy. 7. Consider an explicit domain allowlist for expected newsletter and archive sources. 8. Disable JavaScript unless it is essential. If JavaScript is required, intercept all browser requests and reject requests to disallowed destinations. 9. Run Chromium in an isolated network namespace or container without access to the host, private networks, or cloud metadata services. 10. Add automated tests covering direct private addresses, IPv6 addresses, encoded IP representations, redirects, mixed DNS answers, and DNS rebinding scenarios. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
references/gmail_link_archiver.py:44
Finding
Gmail App Password Is Stored Persistently in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `references/gmail_link_archiver.py:44-58` and `references/gmail_link_archiver.py:79-87` **Vulnerability Type**: Plaintext storage of reusable authentication credentials **Risk Level**: Medium ### Vulnerable Code ```python def load_config() -> Optional[dict]: """Load saved config from local file. Returns None if not found.""" if os.path.isfile(CONFIG_FILE): with open(CONFIG_FILE, "r") as f: return json.load(f) return None def save_config(cfg: dict): """Save config to local file with restricted permissions.""" os.makedirs(CONFIG_DIR, mode=0o700, exist_ok=True) with open(CONFIG_FILE, "w") as f: json.dump(cfg, f, indent=2) os.chmod(CONFIG_FILE, 0o600) print(f"[OK] Config saved to {CONFIG_FILE}") ``` ```python cfg = { "imap_server": imap_server, "imap_port": int(imap_port), "imap_user": imap_user, "imap_password": imap_password, "default_mailbox": default_mailbox, "subject_prefix": subject_prefix, "workspace_path": workspace_path, } save_config(cfg) ``` ### Technical Analysis The application stores the Gmail app password directly in `~/.config/gmail-link-archiver/config.json` as an unencrypted JSON value. Permissions of `0600` reduce exposure to other local user accounts but do not protect the credential from malicious or compromised processes running as the same user, malware with access to the home directory, backups, filesystem snapshots, or accidental copies. The permission restriction is also applied with `os.chmod()` only after the file has been opened, truncated, and written. Although a newly created file will generally be constrained by the process umask, relying on a later permission change is weaker than atomically creating the file with the intended mode. The stored app password is a reusable authentication secret rather than a non-sensitive preference or one-time value. ### Attack Path 1. The user complet ...[truncated 1139 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store the app password in an operating-system credential store, such as Secret Service, GNOME Keyring, KWallet, macOS Keychain, or Windows Credential Manager. 2. Keep only a non-secret credential identifier in `config.json`. 3. Prefer OAuth 2.0 with narrowly scoped and revocable tokens instead of a long-lived app password where practical. 4. Offer a mode that requests the password on each execution without persisting it. 5. If file-based secret storage is unavoidable, encrypt the secret with a key maintained outside the configuration file. 6. Create secret files atomically with restrictive permissions from the outset, for example by using `os.open()` with mode `0o600`, and then write through the returned descriptor. 7. Verify the ownership and permissions of existing configuration files before loading them. Reject symlinks and files owned by unexpected users. 8. Document credential rotation and provide a migration path that removes plaintext secrets from existing configuration files. 9. Ensure logs, exceptions, backups, and diagnostic bundles do not include the secret. ]]>

T08 · Insecure Dependencies

Warning
Location
references/setup.sh:40
Finding
Automatic Installation of Unpinned Dependencies and Browser Components<![CDATA[ ## Vulnerability Details **File Location**: `references/setup.sh:40-55`, `references/gmail_link_archiver.py:204-235`, and `references/gmail_link_archiver.py:266-280` **Vulnerability Type**: Unpinned dependency and executable component installation **Risk Level**: Medium ### Vulnerable Code ```bash # Install pip packages inside the venv echo echo "[SETUP] Installing Python packages into virtual environment..." "$VENV_PIP" install --quiet --upgrade pip "$VENV_PIP" install --quiet playwright html2text # Install Playwright Chromium echo echo "[SETUP] Installing Chromium browser via Playwright..." "$VENV_PYTHON" -m playwright install chromium # Install system dependencies for headless Chromium (Linux) if [ "$(uname)" = "Linux" ]; then echo echo "[SETUP] Installing system dependencies for Chromium..." "$VENV_PYTHON" -m playwright install-deps chromium 2>/dev/null || { echo "[WARN] Could not auto-install system deps." echo " You may need to run: sudo $VENV_PYTHON -m playwright install-deps chromium" } fi ``` ```python def ensure_playwright(): """Ensure Playwright and Chromium are installed. Returns False on failure.""" try: from playwright.sync_api import sync_playwright # noqa: F401 except ImportError: print("[SETUP] Installing Playwright...") try: subprocess.check_call([sys.executable, "-m", "pip", "install", "playwright"]) except subprocess.CalledProcessError as e: print(f"[ERROR] Failed to install Playwright: {e}") return False print("[SETUP] Installing Chromium browser...") try: subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"]) except subprocess.CalledProcessError as e: print(f"[ERROR] Failed to install Chromium: {e}") return False # Install system deps for headless Chromium on Linux (best-effort) try: subprocess.check_call( [sys.execu ...[truncated 3123 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin exact versions of all direct dependencies. 2. Generate a reviewed lockfile that includes transitive dependencies. 3. Require cryptographic hashes during installation, such as with `pip install --require-hashes`. 4. Pin the Playwright release and its corresponding Chromium revision. 5. Use a trusted, explicitly configured package index or an internally controlled artifact repository. 6. Perform dependency installation only during a separate, explicit setup phase. Remove automatic `pip` and browser installation from the runtime application. 7. Avoid silently invoking system dependency installation. Clearly enumerate required operating-system packages and require explicit administrator approval. 8. Do not recommend running Python environment commands with `sudo`; use the operating system's trusted package manager for system dependencies. 9. Verify downloaded artifacts using vendor-provided signatures or checksums where supported. 10. Add automated software-composition analysis and vulnerability scanning for the locked dependency set. 11. Make installation reproducible so that the reviewed source, package versions, hashes, and browser binary uniquely determine the deployed environment. ]]>
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
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (17)

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding
The skill launches subprocesses for pip and Playwright installation, and the analyzer reports shell/process execution capability as undeclared. In an agent platform, undeclared process execution materially raises risk because it allows environment modification and execution of external tooling beyond the core archiving task.

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding
The skill launches subprocesses for pip and Playwright installation, and the analyzer reports shell/process execution capability as undeclared. In an agent platform, undeclared process execution materially raises risk because it allows environment modification and execution of external tooling beyond the core archiving task.

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding
The skill launches subprocesses for pip and Playwright installation, and the analyzer reports shell/process execution capability as undeclared. In an agent platform, undeclared process execution materially raises risk because it allows environment modification and execution of external tooling beyond the core archiving task.

Lp1

High
Category
MCP Least Privilege
Confidence
98% confidence
Finding
The skill launches subprocesses for pip and Playwright installation, and the analyzer reports shell/process execution capability as undeclared. In an agent platform, undeclared process execution materially raises risk because it allows environment modification and execution of external tooling beyond the core archiving task.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
# Reinstall Chromium
python3 -m playwright install chromium
# Install system dependencies (Linux)
sudo python3 -m playwright install-deps chromium
```

**No emails found?**
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
**Permission denied on config file?**
```bash
chmod 600 ~/.config/gmail-link-archiver/config.json
```

## Security
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The skill persistently stores Gmail IMAP credentials, including the app password, in a local JSON config file. While file permissions are tightened, plaintext credential storage on disk increases the blast radius of local compromise, accidental backups, or other processes reading the user's home directory.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The setup interview collects a Gmail app password and later saves it to disk, but the collection flow does not explicitly warn the user at input time that the secret will be stored persistently. In a security-sensitive skill, lack of timely disclosure weakens informed consent and can lead to users exposing long-lived credentials without understanding retention.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
Automatically installing software and system dependencies goes beyond the stated purpose of archiving links from email and meaningfully broadens the trust boundary. In skill contexts, this is dangerous because it combines network fetches, package execution, and host modification without a separate approval step.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
except ImportError:
        print("[SETUP] Installing Playwright...")
        try:
            subprocess.check_call([sys.executable, "-m", "pip", "install", "playwright"])
        except subprocess.CalledProcessError as e:
            print(f"[ERROR] Failed to install Playwright: {e}")
            return False
Confidence
95% confidence
Finding
The script installs the Playwright Python package at runtime via pip. This creates a supply-chain and arbitrary code execution risk because package installation executes code from external repositories in the current environment without a review or explicit approval step.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
print("[SETUP] Installing Chromium browser...")
    try:
        subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"])
    except subprocess.CalledProcessError as e:
        print(f"[ERROR] Failed to install Chromium: {e}")
        return False
Confidence
95% confidence
Finding
The code executes a subprocess to install Chromium at runtime. Although it does not interpolate attacker-controlled shell input, it expands the skill's capabilities by downloading and installing executable software during normal operation, which is risky in constrained agent environments and can lead to unexpected code execution from external package sources.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
# Install system deps for headless Chromium on Linux (best-effort)
    try:
        subprocess.check_call(
            [sys.executable, "-m", "playwright", "install-deps", "chromium"],
            stderr=subprocess.DEVNULL,
        )
Confidence
97% confidence
Finding
This subprocess invokes Playwright's install-deps flow, which may trigger privileged system package installation or modification of host dependencies. In an agent skill, changing system packages exceeds the expected scope of email-link archiving and materially increases host impact if abused or if the dependency source is compromised.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
except ImportError:
        print("[SETUP] Installing html2text...")
        try:
            subprocess.check_call([sys.executable, "-m", "pip", "install", "html2text"])
        except subprocess.CalledProcessError as e:
            print(f"[ERROR] Failed to install html2text: {e}")
            return False
Confidence
94% confidence
Finding
The code installs html2text at runtime through pip, introducing the same supply-chain and environment-modification risk as other dynamic installs. Even though the command is not shell-injected, it still causes unreviewed third-party code to be fetched and executed on the host.

Tainted flow: 'filepath' from input (line 352, user input) → open (file write)

Medium
Category
Data Flow
Content
filename = f"{url_slug}_{url_hash}.md"

    filepath = os.path.join(workspace_path, filename)
    with open(filepath, "w", encoding="utf-8") as f:
        f.write(md_content)

    return filepath
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The pipeline automatically crawls links extracted from emails without a confirmation step at the point of outbound access. Because email content is attacker-controllable, this can trigger requests to malicious or tracking URLs, leak IP/browser metadata, or reach internal network targets if the skill runs in a trusted environment.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if [ -z "$PYTHON" ]; then
    echo "[ERROR] Python 3 is required but not found."
    echo "Install with: sudo apt-get install python3 python3-pip"
    exit 1
fi
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if [ -z "$PYTHON" ]; then
    echo "[ERROR] Python 3 is required but not found."
    echo "Install with: sudo apt-get install python3 python3-pip"
    exit 1
fi
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Static analysis

No suspicious patterns detected.