Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
3 changes: 2 additions & 1 deletion hooks/security_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def debug_log(msg):

def get_state_file(session_id):
"""Return path to session-specific state file for dedup."""
return os.path.expanduser(f"~/.claude/.seal_guard_state_{session_id}.json")
safe_session_id = re.sub(r"[^a-zA-Z0-9_-]", "_", str(session_id))
return os.path.expanduser(f"~/.claude/.seal_guard_state_{safe_session_id}.json")


def load_shown(session_id):
Expand Down
24 changes: 24 additions & 0 deletions hooks/test_security_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest
import os

# Mocking os.path.expanduser to avoid touching real ~
original_expanduser = os.path.expanduser

def mock_expanduser(path):
return path.replace("~", "/tmp/mock_home")

def test_get_state_file(monkeypatch):
monkeypatch.setattr(os.path, "expanduser", mock_expanduser)

# We need to import get_state_file
import sys
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
from security_guard import get_state_file

path1 = get_state_file("normal_id_123")
assert "/tmp/mock_home/.claude/.seal_guard_state_normal_id_123.json" == path1

# With path traversal attack
path2 = get_state_file("../../../etc/passwd")
# After sanitization it should be:
assert "/tmp/mock_home/.claude/.seal_guard_state__________etc_passwd.json" == path2