Skip to content

Commit 3e8a072

Browse files
DrewDennisonclaude
andcommitted
Add 5 Claude Code hooks security rules (9 sub-rules)
New rules detect common security issues in Claude Code hook scripts: - hooks-no-input-validation: missing try/except around stdin JSON parsing - hooks-unquoted-variable: tainted stdin data flowing to eval/exec sinks - hooks-path-traversal: stdin data used in file ops without path resolution - hooks-relative-script-path: relative path script invocations (./scripts/...) - hooks-sensitive-file-access: stdin data used in file ops without sensitive file filtering Covers Python (taint mode) and Bash (taint + pattern-regex) with full test suites. 83 rules total, all validated and tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6d5c2fe commit 3e8a072

14 files changed

+438
-1
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,5 @@ CWE mapping:
7171

7272
## Current Stats
7373

74-
- 35 YAML rule files, 74 individual sub-rules
74+
- 40 YAML rule files, 83 individual sub-rules
7575
- All rules validated, all tests passing
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import json
2+
import sys
3+
4+
# ruleid: hooks-no-input-validation-python
5+
data = json.loads(sys.stdin.read())
6+
7+
# ruleid: hooks-no-input-validation-python
8+
data = json.load(sys.stdin)
9+
10+
# ok: hooks-no-input-validation-python
11+
try:
12+
data = json.loads(sys.stdin.read())
13+
except (json.JSONDecodeError, ValueError):
14+
sys.exit(1)
15+
16+
# ok: hooks-no-input-validation-python
17+
try:
18+
data = json.load(sys.stdin)
19+
except Exception as e:
20+
print(f"Error: {e}")
21+
sys.exit(1)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
3+
# ruleid: hooks-no-input-validation-bash
4+
eval $INPUT
5+
6+
# ruleid: hooks-no-input-validation-bash
7+
eval "$RESULT"
8+
9+
# ruleid: hooks-no-input-validation-bash
10+
echo $DATA | bash
11+
12+
# ruleid: hooks-no-input-validation-bash
13+
echo $DATA | sh
14+
15+
# ok: hooks-no-input-validation-bash
16+
if [ -n "$INPUT" ]; then
17+
echo "Input is not empty"
18+
fi
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
rules:
2+
- id: hooks-no-input-validation-python
3+
languages: [python]
4+
severity: WARNING
5+
message: >-
6+
Claude Code hook reads stdin without input validation. Wrap json.loads/json.load
7+
calls in try/except to handle malformed or unexpected input gracefully.
8+
metadata:
9+
cwe: "CWE-20: Improper Input Validation"
10+
category: security
11+
confidence: MEDIUM
12+
technology: [claude-code]
13+
references:
14+
- https://docs.anthropic.com/en/docs/claude-code/hooks
15+
patterns:
16+
- pattern-either:
17+
- pattern: json.loads(sys.stdin.read())
18+
- pattern: json.load(sys.stdin)
19+
- pattern-not-inside: |
20+
try:
21+
...
22+
except ...:
23+
...
24+
- id: hooks-no-input-validation-bash
25+
languages: [bash]
26+
severity: WARNING
27+
message: >-
28+
Piping untrusted input directly to eval, bash, or sh is dangerous in Claude Code
29+
hooks. Validate and sanitize input before executing it.
30+
metadata:
31+
cwe: "CWE-20: Improper Input Validation"
32+
category: security
33+
confidence: MEDIUM
34+
technology: [claude-code]
35+
references:
36+
- https://docs.anthropic.com/en/docs/claude-code/hooks
37+
pattern-either:
38+
- pattern: eval $...ARGS
39+
- pattern: echo $...ARGS | bash
40+
- pattern: echo $...ARGS | sh
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json
2+
import sys
3+
import os
4+
import shutil
5+
import pathlib
6+
7+
data = json.loads(sys.stdin.read())
8+
# ruleid: hooks-path-traversal-python
9+
f = open(data["file_path"], "r")
10+
11+
hook_input = json.load(sys.stdin)
12+
# ruleid: hooks-path-traversal-python
13+
os.remove(hook_input["path"])
14+
15+
payload = json.loads(sys.stdin.read())
16+
# ruleid: hooks-path-traversal-python
17+
shutil.copy(payload["source"], "/tmp/dest")
18+
19+
payload = json.loads(sys.stdin.read())
20+
# ruleid: hooks-path-traversal-python
21+
p = pathlib.Path(payload["file"])
22+
23+
# ok: hooks-path-traversal-python
24+
data = json.loads(sys.stdin.read())
25+
safe_path = os.path.realpath(data["file_path"])
26+
f = open(safe_path, "r")
27+
28+
# ok: hooks-path-traversal-python
29+
data = json.loads(sys.stdin.read())
30+
abs_path = os.path.abspath(data["file_path"])
31+
os.remove(abs_path)
32+
33+
# ok: hooks-path-traversal-python
34+
hardcoded = open("/tmp/known_file.txt", "r")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
FILE_PATH=$(cat /dev/stdin | jq -r '.file_path')
4+
# ruleid: hooks-path-traversal-bash
5+
cat $FILE_PATH
6+
7+
TARGET=$(echo "$INPUT" | jq -r '.path')
8+
# ruleid: hooks-path-traversal-bash
9+
rm $TARGET
10+
11+
# ok: hooks-path-traversal-bash
12+
RAW_PATH=$(cat /dev/stdin | jq -r '.file_path')
13+
SAFE_PATH=$(realpath "$RAW_PATH")
14+
cat $SAFE_PATH
15+
16+
# ok: hooks-path-traversal-bash
17+
cat /tmp/known_file.txt
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
rules:
2+
- id: hooks-path-traversal-python
3+
mode: taint
4+
languages: [python]
5+
severity: ERROR
6+
message: >-
7+
Hook input flows into a file path without validation. Claude Code hooks
8+
receive JSON input that may contain user-controlled paths. An attacker
9+
could craft input with path traversal sequences (e.g., '../../etc/passwd')
10+
to read, modify, or delete arbitrary files. Use os.path.realpath() or
11+
os.path.abspath() to resolve and validate paths before file operations.
12+
metadata:
13+
cwe: "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
14+
category: security
15+
confidence: MEDIUM
16+
subcategory: [vuln]
17+
technology: [claude-code]
18+
references:
19+
- https://docs.anthropic.com/en/docs/claude-code/hooks
20+
pattern-sources:
21+
- pattern: json.loads(...)
22+
- pattern: json.load(...)
23+
pattern-sinks:
24+
- patterns:
25+
- pattern: open($SINK, ...)
26+
- focus-metavariable: $SINK
27+
- patterns:
28+
- pattern: os.remove($SINK)
29+
- focus-metavariable: $SINK
30+
- patterns:
31+
- pattern: os.unlink($SINK)
32+
- focus-metavariable: $SINK
33+
- patterns:
34+
- pattern: shutil.copy($SINK, ...)
35+
- focus-metavariable: $SINK
36+
- patterns:
37+
- pattern: shutil.move($SINK, ...)
38+
- focus-metavariable: $SINK
39+
- patterns:
40+
- pattern: pathlib.Path($SINK)
41+
- focus-metavariable: $SINK
42+
pattern-sanitizers:
43+
- pattern: os.path.realpath(...)
44+
- pattern: os.path.abspath(...)
45+
46+
- id: hooks-path-traversal-bash
47+
mode: taint
48+
languages: [bash]
49+
severity: ERROR
50+
message: >-
51+
Hook input flows into a file path without validation. Claude Code hooks
52+
receive JSON input that may contain user-controlled paths. An attacker
53+
could craft input with path traversal sequences (e.g., '../../etc/passwd')
54+
to read, modify, or delete arbitrary files. Use realpath or readlink -f
55+
to resolve and validate paths before file operations.
56+
metadata:
57+
cwe: "CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')"
58+
category: security
59+
confidence: MEDIUM
60+
subcategory: [vuln]
61+
technology: [claude-code]
62+
references:
63+
- https://docs.anthropic.com/en/docs/claude-code/hooks
64+
pattern-sources:
65+
- pattern: |
66+
$VAR=$(... | jq ...)
67+
pattern-sinks:
68+
- patterns:
69+
- pattern-either:
70+
- pattern: cat $SINK
71+
- pattern: rm $SINK
72+
- pattern: cp $SINK ...
73+
- pattern: mv $SINK ...
74+
- pattern-not-inside: |
75+
$X=$(... | jq ...)
76+
pattern-sanitizers:
77+
- pattern: realpath ...
78+
- pattern: readlink -f ...
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/bash
2+
3+
# ruleid: hooks-relative-script-path-bash
4+
source ./scripts/validate.sh
5+
6+
# ruleid: hooks-relative-script-path-bash
7+
bash ./hooks/check.sh
8+
9+
# ruleid: hooks-relative-script-path-bash
10+
sh ./run.sh
11+
12+
# ok: hooks-relative-script-path-bash
13+
source /usr/local/hooks/validate.sh
14+
15+
# ok: hooks-relative-script-path-bash
16+
bash "$CLAUDE_PROJECT_DIR/hooks/check.sh"
17+
18+
# ok: hooks-relative-script-path-bash
19+
source "$HOME/.claude/hooks/hook.sh"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
rules:
2+
- id: hooks-relative-script-path-bash
3+
languages: [bash]
4+
severity: WARNING
5+
message: >-
6+
Relative path used for script invocation in hook. Use absolute paths or
7+
environment variables like $CLAUDE_PROJECT_DIR or $HOME to ensure the
8+
correct script is executed regardless of working directory.
9+
metadata:
10+
cwe: "CWE-426: Untrusted Search Path"
11+
category: security
12+
confidence: MEDIUM
13+
technology: [claude-code]
14+
references:
15+
- https://docs.anthropic.com/en/docs/claude-code/hooks
16+
pattern-regex: (source|bash|sh|\.)\s+\./\S+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import json
2+
import sys
3+
import os
4+
import shutil
5+
6+
data = json.loads(sys.stdin.read())
7+
# ruleid: hooks-sensitive-file-access-python
8+
f = open(data["file_path"], "r")
9+
10+
hook_input = json.load(sys.stdin)
11+
# ruleid: hooks-sensitive-file-access-python
12+
os.remove(hook_input["path"])
13+
14+
payload = json.loads(sys.stdin.read())
15+
# ruleid: hooks-sensitive-file-access-python
16+
shutil.copy(payload["source"], "/tmp/dest")
17+
18+
payload = json.loads(sys.stdin.read())
19+
# ruleid: hooks-sensitive-file-access-python
20+
shutil.move(payload["file"], "/tmp/moved")
21+
22+
# ok: hooks-sensitive-file-access-python
23+
data = json.loads(sys.stdin.read())
24+
path = validate_path(data["file_path"])
25+
f = open(path, "r")
26+
27+
# ok: hooks-sensitive-file-access-python
28+
data = json.loads(sys.stdin.read())
29+
safe_path = os.path.realpath(data["file_path"])
30+
f = open(safe_path, "r")
31+
32+
# ok: hooks-sensitive-file-access-python
33+
hardcoded = open("/tmp/known_file.txt", "r")

0 commit comments

Comments
 (0)