sql_injection_check.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """PostToolUse hook (GLOBAL): SQL 注入风险检查。
  4. 检测修改文件中的 SQL 注入风险模式,覆盖 PHP / Python / JS / Java / Go:
  5. 风险模式(命中即警告):
  6. 1. PHP 用户输入直接入 SQL:$_GET/$_POST/$_REQUEST/$_COOKIE 出现在 SQL 字符串中
  7. 2. PHP 字符串拼接 SQL:"SELECT ..." . $var
  8. 3. PHP 变量插值 SQL(双引号):"SELECT ... $var" / "SELECT ... {$var}"
  9. 4. Python f-string SQL:f"SELECT ... {var}"
  10. 5. Python % 格式化 SQL:"SELECT ... %s" % var(仅当含用户输入标记时)
  11. 6. JS 模板字符串 SQL:`SELECT ... ${var}`
  12. 7. Java 字符串拼接 SQL:"SELECT ..." + var
  13. SQL 关键字覆盖:
  14. - SQL 标准:SELECT / INSERT INTO / UPDATE / DELETE FROM / DROP TABLE /
  15. ALTER TABLE / CREATE TABLE / UNION SELECT / TRUNCATE TABLE
  16. - MySQL/MariaDB 特有:REPLACE INTO / LOAD DATA INFILE / LOAD DATA LOCAL INFILE /
  17. INTO OUTFILE / INTO DUMPFILE / GRANT / REVOKE / CALL
  18. 安全模式(同行出现则跳过,不误报):
  19. - 预处理语句:prepare( / prepare "
  20. - 参数绑定:bindParam / bindValue / bind_param
  21. - ORM 查询构造器:->where( / ->table( / ::table( / .where(
  22. - 批量执行:executemany
  23. 非阻断 exit 1 回显 AI(与 select_star.py 一致)。
  24. """
  25. import sys
  26. import os
  27. import json
  28. import re
  29. # 受检查的文件扩展名
  30. TEXT_EXT = {".php", ".phtml", ".php3", ".php5", ".py", ".js", ".jsx",
  31. ".ts", ".tsx", ".java", ".go", ".rb", ".sql", ".inc"}
  32. # SQL 关键字前缀(匹配 SQL 语句开头)
  33. # 覆盖 SQL 标准 + MySQL/MariaDB/PostgreSQL 特有高危语法
  34. SQL_KEYWORDS = (
  35. r"(?:SELECT\s|INSERT\s+INTO|UPDATE\s|DELETE\s+FROM|DROP\s+TABLE|"
  36. r"ALTER\s+TABLE|CREATE\s+TABLE|UNION\s+SELECT|TRUNCATE\s+TABLE|"
  37. r"REPLACE\s+INTO|"
  38. r"LOAD\s+DATA\s+INFILE|LOAD\s+DATA\s+LOCAL\s+INFILE|" # MySQL/MariaDB 读文件
  39. r"INTO\s+OUTFILE|INTO\s+DUMPFILE|" # MySQL/MariaDB 写文件
  40. r"GRANT\s|REVOKE\s|" # 权限操纵
  41. r"CALL\s)" # 存储过程调用
  42. )
  43. # 风险模式:(正则, 描述)
  44. RISK_PATTERNS = [
  45. # PHP: $_GET/$_POST/$_REQUEST/$_COOKIE 直接出现在 SQL 字符串中
  46. (re.compile(
  47. r'["\'][^"\']*' + SQL_KEYWORDS + r'[^"\']*\$_(?:GET|POST|REQUEST|COOKIE|SERVER)\s*\[',
  48. re.IGNORECASE),
  49. "PHP 用户输入直接入 SQL($_GET/$_POST/$_REQUEST)"),
  50. # PHP: 字符串拼接 SQL "SELECT ..." . $var 或 "SELECT ..." . $obj->prop
  51. (re.compile(
  52. r'["\']\s*' + SQL_KEYWORDS + r'.*?["\']\s*\.\s*\$',
  53. re.IGNORECASE),
  54. "PHP 字符串拼接 SQL"),
  55. # PHP: 双引号变量插值 SQL "SELECT ... $var" 或 "SELECT ... {$var}"
  56. (re.compile(
  57. r'"[^"]*' + SQL_KEYWORDS + r'[^"]*\$(?:[a-zA-Z_]|\{)',
  58. re.IGNORECASE),
  59. "PHP 变量插值 SQL(双引号内 $var)"),
  60. # Python: f-string SQL f"SELECT ... {var}" 或 f'SELECT ... {var}'
  61. (re.compile(
  62. r'f["\']\s*' + SQL_KEYWORDS + r'[^"\']*\{',
  63. re.IGNORECASE),
  64. "Python f-string SQL(变量插值)"),
  65. # Python: % 格式化 SQL "SELECT ... %s" % var(仅当 var 是用户输入相关时)
  66. (re.compile(
  67. r'["\']\s*' + SQL_KEYWORDS + r'[^"\']*%s[^"\']*["\']\s*%\s*\(',
  68. re.IGNORECASE),
  69. "Python % 格式化 SQL(疑似拼接)"),
  70. # JS: 模板字符串 SQL `SELECT ... ${var}`
  71. (re.compile(
  72. r'`\s*' + SQL_KEYWORDS + r'[^`]*\$\{',
  73. re.IGNORECASE),
  74. "JS 模板字符串 SQL(变量插值)"),
  75. # Java: 字符串拼接 SQL "SELECT ..." + var
  76. (re.compile(
  77. r'"\s*' + SQL_KEYWORDS + r'[^"]*"\s*\+\s*\w',
  78. re.IGNORECASE),
  79. "Java 字符串拼接 SQL"),
  80. ]
  81. # 安全模式(同行出现则跳过)
  82. SAFE_PATTERNS = [
  83. re.compile(r'\bprepare\s*\(', re.IGNORECASE), # PDO/MySQLi prepare
  84. re.compile(r'\bprepare\s+"', re.IGNORECASE), # Python DB-API prepare
  85. re.compile(r'\bbindParam\b', re.IGNORECASE), # PDO bindParam
  86. re.compile(r'\bbindValue\b', re.IGNORECASE), # PDO bindValue
  87. re.compile(r'\bbind_param\b', re.IGNORECASE), # MySQLi bind_param
  88. re.compile(r'->where\s*\(', re.IGNORECASE), # ORM where
  89. re.compile(r'->table\s*\(', re.IGNORECASE), # ORM table
  90. re.compile(r'::table\s*\(', re.IGNORECASE), # ORM static table
  91. re.compile(r'\.where\s*\(', re.IGNORECASE), # JS ORM where
  92. re.compile(r'\bexecutemany\b', re.IGNORECASE), # Python executemany
  93. ]
  94. # 跳过注释行的逻辑(与 select_star.py 一致)
  95. def _is_comment(s):
  96. return (s.startswith("//") or s.startswith("#") or
  97. s.startswith("*") or s.startswith("/*"))
  98. def find_sql_injection_risks(text):
  99. """逐行扫描,跳过注释,返回 (行号, 描述, 行内容) 列表。
  100. 对每行:
  101. 1. 跳过注释行
  102. 2. 检查是否含风险模式
  103. 3. 若命中风险模式,检查同行是否有安全模式(有则跳过)
  104. """
  105. hits = []
  106. in_block = False
  107. for i, line in enumerate(text.splitlines(), start=1):
  108. s = line.strip()
  109. # 块注释处理
  110. if in_block:
  111. if "*/" in line:
  112. in_block = False
  113. continue
  114. if "/*" in line:
  115. if "*/" in line:
  116. continue
  117. in_block = True
  118. continue
  119. # 单行注释跳过
  120. if _is_comment(s):
  121. continue
  122. # 检查风险模式
  123. for pattern, desc in RISK_PATTERNS:
  124. if pattern.search(line):
  125. # 检查同行是否有安全模式
  126. is_safe = any(sp.search(line) for sp in SAFE_PATTERNS)
  127. if not is_safe:
  128. hits.append((i, desc, s[:100]))
  129. break # 同行只报一次
  130. return hits
  131. def main():
  132. try:
  133. raw = sys.stdin.read()
  134. if not raw.strip():
  135. return 0
  136. data = json.loads(raw)
  137. except Exception:
  138. return 0
  139. tool_input = data.get("tool_input", {}) or {}
  140. path = (
  141. tool_input.get("filePath")
  142. or tool_input.get("file_path")
  143. or tool_input.get("path")
  144. or ""
  145. )
  146. if not path:
  147. return 0
  148. path = os.path.abspath(path)
  149. if os.path.splitext(path)[1].lower() not in TEXT_EXT:
  150. return 0
  151. if not os.path.isfile(path):
  152. return 0
  153. try:
  154. with open(path, "r", encoding="utf-8", errors="ignore") as f:
  155. text = f.read()
  156. except Exception:
  157. return 0
  158. hits = find_sql_injection_risks(text)
  159. if hits:
  160. print("[SQL-INJECTION-WARN] 发现 SQL 注入风险(字符串拼接/变量插值):")
  161. for line_no, desc, preview in hits:
  162. print(" 行 %d [%s] %s" % (line_no, desc, preview))
  163. print("建议改用预处理语句(prepare + bindParam/bindValue)或 ORM 查询构造器。")
  164. return 1
  165. return 0
  166. if __name__ == "__main__":
  167. sys.exit(main())