| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """PostToolUse hook (GLOBAL): SQL 注入风险检查。
- 检测修改文件中的 SQL 注入风险模式,覆盖 PHP / Python / JS / Java / Go:
- 风险模式(命中即警告):
- 1. PHP 用户输入直接入 SQL:$_GET/$_POST/$_REQUEST/$_COOKIE 出现在 SQL 字符串中
- 2. PHP 字符串拼接 SQL:"SELECT ..." . $var
- 3. PHP 变量插值 SQL(双引号):"SELECT ... $var" / "SELECT ... {$var}"
- 4. Python f-string SQL:f"SELECT ... {var}"
- 5. Python % 格式化 SQL:"SELECT ... %s" % var(仅当含用户输入标记时)
- 6. JS 模板字符串 SQL:`SELECT ... ${var}`
- 7. Java 字符串拼接 SQL:"SELECT ..." + var
- SQL 关键字覆盖:
- - SQL 标准:SELECT / INSERT INTO / UPDATE / DELETE FROM / DROP TABLE /
- ALTER TABLE / CREATE TABLE / UNION SELECT / TRUNCATE TABLE
- - MySQL/MariaDB 特有:REPLACE INTO / LOAD DATA INFILE / LOAD DATA LOCAL INFILE /
- INTO OUTFILE / INTO DUMPFILE / GRANT / REVOKE / CALL
- 安全模式(同行出现则跳过,不误报):
- - 预处理语句:prepare( / prepare "
- - 参数绑定:bindParam / bindValue / bind_param
- - ORM 查询构造器:->where( / ->table( / ::table( / .where(
- - 批量执行:executemany
- 非阻断 exit 1 回显 AI(与 select_star.py 一致)。
- """
- import sys
- import os
- import json
- import re
- # 受检查的文件扩展名
- TEXT_EXT = {".php", ".phtml", ".php3", ".php5", ".py", ".js", ".jsx",
- ".ts", ".tsx", ".java", ".go", ".rb", ".sql", ".inc"}
- # SQL 关键字前缀(匹配 SQL 语句开头)
- # 覆盖 SQL 标准 + MySQL/MariaDB/PostgreSQL 特有高危语法
- SQL_KEYWORDS = (
- r"(?:SELECT\s|INSERT\s+INTO|UPDATE\s|DELETE\s+FROM|DROP\s+TABLE|"
- r"ALTER\s+TABLE|CREATE\s+TABLE|UNION\s+SELECT|TRUNCATE\s+TABLE|"
- r"REPLACE\s+INTO|"
- r"LOAD\s+DATA\s+INFILE|LOAD\s+DATA\s+LOCAL\s+INFILE|" # MySQL/MariaDB 读文件
- r"INTO\s+OUTFILE|INTO\s+DUMPFILE|" # MySQL/MariaDB 写文件
- r"GRANT\s|REVOKE\s|" # 权限操纵
- r"CALL\s)" # 存储过程调用
- )
- # 风险模式:(正则, 描述)
- RISK_PATTERNS = [
- # PHP: $_GET/$_POST/$_REQUEST/$_COOKIE 直接出现在 SQL 字符串中
- (re.compile(
- r'["\'][^"\']*' + SQL_KEYWORDS + r'[^"\']*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\s*\[',
- re.IGNORECASE),
- "PHP 用户输入直接入 SQL($_GET/$_POST/$_REQUEST)"),
- # PHP: 字符串拼接 SQL "SELECT ..." . $var 或 "SELECT ..." . $obj->prop
- (re.compile(
- r'["\']\s*' + SQL_KEYWORDS + r'.*?["\']\s*\.\s*\$',
- re.IGNORECASE),
- "PHP 字符串拼接 SQL"),
- # PHP: 双引号变量插值 SQL "SELECT ... $var" 或 "SELECT ... {$var}"
- (re.compile(
- r'"[^"]*' + SQL_KEYWORDS + r'[^"]*\$(?:[a-zA-Z_]|\{)',
- re.IGNORECASE),
- "PHP 变量插值 SQL(双引号内 $var)"),
- # Python: f-string SQL f"SELECT ... {var}" 或 f'SELECT ... {var}'
- (re.compile(
- r'f["\']\s*' + SQL_KEYWORDS + r'[^"\']*\{',
- re.IGNORECASE),
- "Python f-string SQL(变量插值)"),
- # Python: % 格式化 SQL "SELECT ... %s" % var(仅当 var 是用户输入相关时)
- (re.compile(
- r'["\']\s*' + SQL_KEYWORDS + r'[^"\']*%s[^"\']*["\']\s*%\s*\(',
- re.IGNORECASE),
- "Python % 格式化 SQL(疑似拼接)"),
- # JS: 模板字符串 SQL `SELECT ... ${var}`
- (re.compile(
- r'`\s*' + SQL_KEYWORDS + r'[^`]*\$\{',
- re.IGNORECASE),
- "JS 模板字符串 SQL(变量插值)"),
- # Java: 字符串拼接 SQL "SELECT ..." + var
- (re.compile(
- r'"\s*' + SQL_KEYWORDS + r'[^"]*"\s*\+\s*\w',
- re.IGNORECASE),
- "Java 字符串拼接 SQL"),
- ]
- # 安全模式(同行出现则跳过)
- SAFE_PATTERNS = [
- re.compile(r'\bprepare\s*\(', re.IGNORECASE), # PDO/MySQLi prepare
- re.compile(r'\bprepare\s+"', re.IGNORECASE), # Python DB-API prepare
- re.compile(r'\bbindParam\b', re.IGNORECASE), # PDO bindParam
- re.compile(r'\bbindValue\b', re.IGNORECASE), # PDO bindValue
- re.compile(r'\bbind_param\b', re.IGNORECASE), # MySQLi bind_param
- re.compile(r'->where\s*\(', re.IGNORECASE), # ORM where
- re.compile(r'->table\s*\(', re.IGNORECASE), # ORM table
- re.compile(r'::table\s*\(', re.IGNORECASE), # ORM static table
- re.compile(r'\.where\s*\(', re.IGNORECASE), # JS ORM where
- re.compile(r'\bexecutemany\b', re.IGNORECASE), # Python executemany
- ]
- # 跳过注释行的逻辑(与 select_star.py 一致)
- def _is_comment(s):
- return (s.startswith("//") or s.startswith("#") or
- s.startswith("*") or s.startswith("/*"))
- def find_sql_injection_risks(text):
- """逐行扫描,跳过注释,返回 (行号, 描述, 行内容) 列表。
- 对每行:
- 1. 跳过注释行
- 2. 检查是否含风险模式
- 3. 若命中风险模式,检查同行是否有安全模式(有则跳过)
- """
- hits = []
- in_block = False
- for i, line in enumerate(text.splitlines(), start=1):
- s = line.strip()
- # 块注释处理
- if in_block:
- if "*/" in line:
- in_block = False
- continue
- if "/*" in line:
- if "*/" in line:
- continue
- in_block = True
- continue
- # 单行注释跳过
- if _is_comment(s):
- continue
- # 检查风险模式
- for pattern, desc in RISK_PATTERNS:
- if pattern.search(line):
- # 检查同行是否有安全模式
- is_safe = any(sp.search(line) for sp in SAFE_PATTERNS)
- if not is_safe:
- hits.append((i, desc, s[:100]))
- break # 同行只报一次
- return hits
- def main():
- try:
- raw = sys.stdin.read()
- if not raw.strip():
- return 0
- data = json.loads(raw)
- except Exception:
- return 0
- tool_input = data.get("tool_input", {}) or {}
- path = (
- tool_input.get("filePath")
- or tool_input.get("file_path")
- or tool_input.get("path")
- or ""
- )
- if not path:
- return 0
- path = os.path.abspath(path)
- if os.path.splitext(path)[1].lower() not in TEXT_EXT:
- return 0
- if not os.path.isfile(path):
- return 0
- try:
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
- text = f.read()
- except Exception:
- return 0
- hits = find_sql_injection_risks(text)
- if hits:
- print("[SQL-INJECTION-WARN] 发现 SQL 注入风险(字符串拼接/变量插值):")
- for line_no, desc, preview in hits:
- print(" 行 %d [%s] %s" % (line_no, desc, preview))
- print("建议改用预处理语句(prepare + bindParam/bindValue)或 ORM 查询构造器。")
- return 1
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|