Skip to content

Security: SSRF, cache poisoning & symlink overwrite in opt-in lifecycle hooks (sdd-cache, simplify-ignore) #295

@bill143

Description

@bill143

Summary

While evaluating the lifecycle hooks in hooks/, a multi-pass review (correctness, security, and test-coverage) surfaced several security and data-integrity issues in the opt-in hooks — sdd-cache-pre.sh, sdd-cache-post.sh, and simplify-ignore.sh.

These hooks are not wired by default (hooks/hooks.json only registers session-start.sh, which is fine), so this affects users who manually enable the WebFetch cache or the simplify-ignore feature per SDD-CACHE.md / SIMPLIFY-IGNORE.md. Filing publicly since there's no SECURITY.md / private channel. Happy to convert to a private report if you'd prefer.

session-start.sh reviewed clean (trusted local input, correct jq --arg escaping).

Findings

# Severity File Class Issue
1 Critical sdd-cache-pre.sh:71-74, sdd-cache-post.sh:82 SSRF curl -L "$URL" with no scheme/host allowlist, following redirects, fires automatically on every WebFetch
2 High sdd-cache-post.sh:105-128, sdd-cache-pre.sh:62-106 Cache poisoning / prompt injection No TTL cap; any 304 serves cached body forever as authoritative "unchanged" content
3 High sdd-cache-post.sh:75-77, sdd-cache-pre.sh:50-51 Tamperable cache Predictable filenames (sha256(URL)), default perms, in-tree .claude/sdd-cache/; a committed/crafted cache entry is trusted on next 304
4 High simplify-ignore.sh:153-154,202,209,287,291 Symlink overwrite / TOCTOU Backup/restore writes via cat > "$target" on an untrusted file_path with no symlink check; arbitrary-file-overwrite primitive
5 Critical (integrity) simplify-ignore.sh filter/expand path Data loss Non-atomic in-place rewrite under set -e; an abort (e.g. missing perl, full disk) can leave the real file holding placeholders after the backup was overwritten

1. SSRF via unvalidated curl (Critical)

URL comes verbatim from .tool_input.url and is passed to curl ... -L with no scheme/host validation and redirect-following enabled. A WebFetch to an attacker-influenced link (e.g. via prompt injection in a fetched page) can 302-redirect the hook's automatic out-of-band request to internal targets — cloud metadata (169.254.169.254), localhost admin ports, etc.

Highest-value single fix: drop -L (or re-validate the host on every hop), require https://, and reject internal/metadata hosts before any curl.

2. Durable cache poisoning (High)

fetched_at is stored but never enforced as a max age. Once an entry is cached, any subsequent 304 Not Modified causes the stored body to be served to the agent verbatim and labeled "Revalidated … unchanged" — with no TTL. A malicious or compromised origin (or MITM on a non-HTTPS URL via #1) can pin injected content that persists across sessions.

Fix: enforce a TTL cap before serving from cache regardless of 304; optionally store + verify a content hash.

3. Tamperable / disclosing cache (High)

Cache files are written to ${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache/<sha>.json with default umask and no integrity protection; the filename is sha256(URL), fully predictable. A crafted entry (even one committed to a repo) is trusted and served on the next matching WebFetch + 304. Fetched bodies (possibly authenticated/internal content) are persisted in plaintext inside the working tree.

Fix: chmod 700 the cache dir / umask 077; add .claude/sdd-cache/ to .gitignore guidance; don't trust pre-existing entries without an integrity check.

4. Symlink/arbitrary-file overwrite (High)

file_path (untrusted) drives backup-and-rewrite on Read and write-back on Edit/Write/Stop via cat > "$orig", which follows symlinks. There's no [ -L ] check and the path→file mapping is a 16-char hash with no inode re-verification, opening a session-long TOCTOU window. This is an arbitrary-overwrite primitive against any user-writable file (~/.bashrc, etc.).

Fix: [ -L "$FILE_PATH" ] && exit 0 at entry; re-verify the target is a regular non-symlink (by inode) before every write-back; write via temp + mv -T.

5. Non-atomic rewrite under set -e (Critical integrity / data loss)

simplify-ignore.sh rewrites the user's real file in place across multiple hook events. With set -euo pipefail, an unguarded failure inside filter_file (e.g. perl absent on a minimal Git Bash — line 137/269 hard-depend on perl though only jq/shasum are declared; or a full/read-only cache dir) can abort after : > "$dest" and after the backup was overwritten, leaving the on-disk file full of BLOCK_<hash> placeholders and no clean backup.

Fix: declare perl as a dependency and check up front (or replace with pure-shell newline handling); trap-guard the rewrite; make the final swap atomic (mv temp into place).

Additional notes

  • simplify-ignore.sh file identity is sha(path)|cut -c1-16 with no path normalization — the same file as C:\… vs /c/… can be filtered under two IDs, and Stop can restore stale content over edits. Normalize the path (e.g. cygpath/realpath) before hashing.
  • BLOCK_<hash> placeholder expansion uses global substring replace with fuzzy/last-resort fallbacks; protected content that itself contains a BLOCK_<hash>-shaped string can be corrupted on expand. Consider a sentinel that cannot occur in source.
  • The two sdd-cache scripts have no tests; the data-loss-critical expand/restore half of simplify-ignore.sh is also untested. Highest-value missing test: round-trip fidelity (filter → expand → original bytes recovered).

Suggested minimum changes to make the opt-in hooks safe to enable

  1. sdd-cache-*: require https://, drop -L (or re-validate host per hop), block internal/metadata hosts. (fixes Add Claude Code plugin support and fix stale swe-skills references #1)
  2. sdd-cache-*: enforce a TTL cap before serving cache; chmod 700 the cache dir; don't trust pre-existing entries. (fixes Implement /code-simplify skill #2, Fixes #2 - adds code simplifier v0.1 #3)
  3. simplify-ignore.sh: reject symlinks and re-verify a regular-file target before write-back; atomic, trap-guarded rewrite; declare/​check perl. (fixes docs: add code-simplification to README #4, [Bug]: browser-testing-with-devtools - Gen Agent Trust failed & Snyk warnings #5)

Methodology: findings independently corroborated across three review passes. Glad to follow up with PRs for any of the above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions