Files
gh-dennisliuck-claude-plugi…/hooks/security_reminder_hook.py
2025-11-29 18:18:35 +08:00

354 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
安全提醒鉤子 - Claude Code
此鉤子在檔案編輯前檢測潛在的安全漏洞。
它會分析檔案內容並警告常見的安全問題。
"""
import json
import sys
import os
import re
from pathlib import Path
from datetime import datetime, timedelta
# 定義安全模式及其警告訊息
SECURITY_PATTERNS = [
{
"name": "GitHub Actions 工作流程注入",
"pattern": r'\$\{\{\s*github\.(event|head_ref|base_ref)',
"description": "GitHub Actions 工作流程中的潛在命令注入",
"warning": """
⚠️ 安全警告GitHub Actions 工作流程注入
偵測到在 shell 命令中使用未經淨化的 GitHub 事件內容。
這可能導致命令注入漏洞。
建議:
- 避免直接在 shell 命令中使用 ${{{{ github.event.* }}}}
- 使用環境變數並適當地引用
- 考慮使用 GitHub Actions 的內建功能
參考https://securitylab.github.com/research/github-actions-untrusted-input/
""",
"confidence": 0.9
},
{
"name": "子程序執行",
"pattern": r'\.exec\s*\(',
"description": "使用不安全的子程序執行方法",
"warning": """
⚠️ 安全警告:不安全的子程序執行
偵測到使用 .exec(),這可能導致 shell 注入漏洞。
建議:
- 改用 execFile() 或 spawn()
- 如果必須使用 exec(),請驗證和淨化所有輸入
- 考慮使用白名單方式處理命令
Node.js 範例:
// 不安全
exec(`command ${userInput}`)
// 較安全
execFile('command', [arg1, arg2])
""",
"confidence": 0.8
},
{
"name": "動態程式碼評估",
"pattern": r'(new\s+Function|eval)\s*\(',
"description": "使用 eval 或 new Function",
"warning": """
⚠️ 安全警告:動態程式碼評估
偵測到使用 eval() 或 new Function(),這可能導致程式碼注入漏洞。
建議:
- 避免使用 eval() 和 new Function()
- 使用 JSON.parse() 處理 JSON 資料
- 使用靜態程式碼分析工具
- 如果無法避免,嚴格驗證輸入
替代方案:
- 使用物件字面量或 Map
- 使用範本引擎(如 Handlebars、Mustache
- 重構程式碼以避免動態評估
""",
"confidence": 0.95
},
{
"name": "基於 DOM 的 XSS",
"pattern": r'(dangerouslySetInnerHTML|innerHTML|document\.write)\s*[=\(]',
"description": "潛在的跨站腳本XSS漏洞",
"warning": """
⚠️ 安全警告:潛在的 XSS 漏洞
偵測到使用可能導致跨站腳本攻擊的方法。
風險:
- dangerouslySetInnerHTMLReact 中的 XSS 風險
- innerHTML直接 DOM 操作的 XSS 風險
- document.write():可能被利用注入惡意腳本
建議:
- 使用 textContent 而非 innerHTML
- 在 React 中使用 JSX 來渲染內容
- 如果必須渲染 HTML使用 DOMPurify 等清理庫
- 實施內容安全政策CSP
React 安全範例:
// 不安全
<div dangerouslySetInnerHTML={{{{__html: userInput}}}} />
// 較安全
<div>{{userInput}}</div> // 自動轉義
""",
"confidence": 0.85
},
{
"name": "Python Pickle 反序列化",
"pattern": r'pickle\.loads?\s*\(',
"description": "不安全的 pickle 反序列化",
"warning": """
⚠️ 安全警告:不安全的反序列化
偵測到使用 pickle.load() 或 pickle.loads(),這可能導致任意程式碼執行。
風險:
- Pickle 可以執行任意 Python 程式碼
- 不受信任的 pickle 資料可能包含惡意載荷
- 攻擊者可能獲得系統完全控制權
建議:
- 永遠不要反序列化不受信任的資料
- 使用 JSON 或其他安全的序列化格式
- 如果必須使用 pickle驗證資料來源
- 考慮使用 hmac 簽名驗證資料完整性
替代方案:
import json
data = json.loads(json_string) # 安全的替代方案
""",
"confidence": 0.9
},
{
"name": "作業系統命令注入",
"pattern": r'os\.system\s*\(',
"description": "潛在的命令注入漏洞",
"warning": """
⚠️ 安全警告:作業系統命令注入
偵測到使用 os.system(),這容易受到命令注入攻擊。
風險:
- Shell 元字元可能被利用執行任意命令
- 使用者輸入未經淨化可能導致系統入侵
- 可能洩露敏感資訊
建議:
- 使用 subprocess.run() 並傳遞參數列表
- 永遠不要直接拼接使用者輸入到命令中
- 使用 shlex.quote() 淨化輸入
- 實施最小權限原則
安全範例:
# 不安全
os.system(f"ls {user_input}")
# 較安全
subprocess.run(['ls', user_input], check=True)
""",
"confidence": 0.9
},
{
"name": "SQL 注入風險",
"pattern": r'(execute|query)\s*\(\s*[\'"`].*%s.*[\'"`]|f[\'"`].*SELECT.*FROM',
"description": "潛在的 SQL 注入漏洞",
"warning": """
⚠️ 安全警告SQL 注入風險
偵測到可能的 SQL 注入漏洞。
風險:
- 字串拼接可能導致 SQL 注入
- 攻擊者可能讀取、修改或刪除資料庫資料
- 可能繞過身份驗證和授權
建議:
- 使用參數化查詢或預備語句
- 使用 ORM如 SQLAlchemy、Django ORM
- 永遠不要直接拼接使用者輸入到 SQL 查詢
- 實施最小權限資料庫存取
安全範例:
# 不安全
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# 較安全
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
""",
"confidence": 0.85
},
{
"name": "硬編碼密鑰",
"pattern": r'(password|secret|api[_-]?key|token)\s*=\s*[\'"`][A-Za-z0-9+/=]{8,}[\'"`]',
"description": "可能的硬編碼敏感資訊",
"warning": """
⚠️ 安全警告:硬編碼敏感資訊
偵測到可能的硬編碼密碼、金鑰或令牌。
風險:
- 敏感資訊可能被提交到版本控制系統
- 原始碼洩露可能導致未授權存取
- 難以輪換和管理密鑰
建議:
- 使用環境變數儲存敏感資訊
- 使用密鑰管理服務(如 AWS Secrets Manager、HashiCorp Vault
- 永遠不要將密鑰提交到版本控制
- 使用 .env 檔案並將其加入 .gitignore
最佳實踐:
# 不安全
api_key = "sk_live_abc123xyz789"
# 較安全
api_key = os.getenv('API_KEY')
""",
"confidence": 0.95
}
]
def load_session_state(session_id):
"""載入此工作階段已顯示警告的狀態"""
state_dir = Path.home() / '.claude'
state_dir.mkdir(exist_ok=True)
state_file = state_dir / f'security_warnings_state_{session_id}.json'
# 清理超過 30 天的舊狀態檔案
cleanup_old_state_files(state_dir)
if state_file.exists():
try:
with open(state_file, 'r', encoding='utf-8') as f:
return json.load(f)
except:
return {}
return {}
def save_session_state(session_id, state):
"""儲存此工作階段已顯示警告的狀態"""
state_dir = Path.home() / '.claude'
state_dir.mkdir(exist_ok=True)
state_file = state_dir / f'security_warnings_state_{session_id}.json'
with open(state_file, 'w', encoding='utf-8') as f:
json.dump(state, f, ensure_ascii=False)
def cleanup_old_state_files(state_dir):
"""清理超過 30 天的狀態檔案"""
try:
cutoff = datetime.now() - timedelta(days=30)
for file in state_dir.glob('security_warnings_state_*.json'):
if datetime.fromtimestamp(file.stat().st_mtime) < cutoff:
file.unlink()
except:
pass
def check_security_patterns(file_path, content):
"""檢查內容中的安全模式"""
warnings = []
for pattern_info in SECURITY_PATTERNS:
pattern = pattern_info["pattern"]
if re.search(pattern, content, re.MULTILINE | re.IGNORECASE):
warnings.append({
"name": pattern_info["name"],
"description": pattern_info["description"],
"warning": pattern_info["warning"],
"confidence": pattern_info["confidence"],
"file": file_path
})
return warnings
def main():
"""主要執行函式"""
# 檢查是否啟用安全提醒
if os.getenv('ENABLE_SECURITY_REMINDER', 'true').lower() == 'false':
sys.exit(0)
try:
# 從 stdin 讀取輸入
input_data = json.loads(sys.stdin.read())
session_id = input_data.get('sessionId', 'default')
tool_name = input_data.get('toolName', '')
file_path = input_data.get('filePath', '')
# 載入工作階段狀態
session_state = load_session_state(session_id)
# 提取檔案內容
content = ""
if tool_name == "Write":
content = input_data.get('content', '')
elif tool_name == "Edit":
content = input_data.get('newString', '')
elif tool_name == "MultiEdit":
edits = input_data.get('edits', [])
content = '\n'.join([edit.get('newString', '') for edit in edits])
# 檢查安全模式
warnings = check_security_patterns(file_path, content)
# 過濾已顯示過的警告
new_warnings = []
for warning in warnings:
warning_key = f"{file_path}:{warning['name']}"
if warning_key not in session_state:
new_warnings.append(warning)
session_state[warning_key] = True
# 如果有新警告,顯示並阻止執行
if new_warnings:
# 儲存狀態
save_session_state(session_id, session_state)
# 顯示警告
print("=" * 80, file=sys.stderr)
print(f"檔案:{file_path}", file=sys.stderr)
print("=" * 80, file=sys.stderr)
for warning in new_warnings:
print(warning['warning'], file=sys.stderr)
print("-" * 80, file=sys.stderr)
print("\n如果您確定這些變更是安全的,可以繼續執行。", file=sys.stderr)
print("若要停用此警告請設定環境變數ENABLE_SECURITY_REMINDER=false", file=sys.stderr)
print("=" * 80, file=sys.stderr)
# 返回代碼 2 表示阻止但允許重試
sys.exit(2)
# 沒有警告或已顯示過,允許繼續
sys.exit(0)
except Exception as e:
# 發生錯誤時不阻止執行
print(f"安全檢查發生錯誤:{e}", file=sys.stderr)
sys.exit(0)
if __name__ == '__main__':
main()