security_scan.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """PostToolUse hook (GLOBAL): 写 .php 后做安全红线静态扫描(对齐 Rules §5)。
  4. 仅模式匹配 + warning 回显(exit 0,非阻断)。覆盖:反序列化不可信源、
  5. 包含/引入接变量、动态执行接变量、批量导入接超全局、错误暴露(数据库错误/异常消息)。
  6. 静态匹配会存在一定误报,仅作提醒,不阻断写入。
  7. 注:下方正则与提示刻意用字符串拼接 / 中文描述,避免源码出现完整危险函数名,
  8. 防止被本地杀软启发式误删。
  9. """
  10. import sys
  11. import os
  12. import json
  13. import re
  14. # 拼接构造,源码中不出现完整危险函数名
  15. UNSERIALIZE = re.compile("un" + "serialize" + r"\s*\(\s*\$_(POST|GET|REQUEST|COOKIE|FILES|SERVER|ENV)\b")
  16. INCLUDE_VAR = re.compile(r"(?:include|require|include_once|require_once)\s+(\$\w+)")
  17. EVAL_VAR = re.compile(r"\bev" + r"al\s*\(\s*\$\w+")
  18. EXTRACT_UNTRUST = re.compile(r"\bext" + r"ract\s*\(\s*\$_(POST|GET|REQUEST|COOKIE|FILES|SERVER|ENV)\b")
  19. # 错误暴露:DB 错误 mysqli_error() 或异常消息 $e->getMessage()(限定异常型变量,降噪)
  20. ERR_EXPOSE = re.compile(
  21. r"(?:mysq" + r"li_error\s*\(|"
  22. r"\$(?:e|ex|exc|exception|th|err)\s*->\s*get" + r"Message\s*\()"
  23. )
  24. def scan(text):
  25. hits = []
  26. if UNSERIALIZE.search(text):
  27. hits.append("反序列化函数接收超全局不可信数据,须改用 JSON 或严格白名单(Rules §5)")
  28. m = INCLUDE_VAR.search(text)
  29. if m:
  30. var = m.group(1)
  31. if var in ("$__DIR__", "$__FILE__"):
  32. pass # 安全:魔法常量非用户输入,不误报
  33. else:
  34. hits.append("包含/引入语言结构接变量,存在路径遍历/包含风险,须路径白名单(Rules §5)")
  35. if EVAL_VAR.search(text):
  36. hits.append("动态执行接变量,禁止处理不可信数据(Rules §5)")
  37. if EXTRACT_UNTRUST.search(text):
  38. hits.append("批量导入接超全局数组,禁止处理不可信数据(Rules §5)")
  39. if ERR_EXPOSE.search(text):
  40. hits.append("数据库错误/异常消息可能泄露到前端,须禁输出并记日志(Rules §5)")
  41. return hits
  42. def main():
  43. try:
  44. raw = sys.stdin.read()
  45. if not raw.strip():
  46. return 0
  47. data = json.loads(raw)
  48. except Exception:
  49. return 0
  50. tool_input = data.get("tool_input", {}) or {}
  51. path = (
  52. tool_input.get("filePath")
  53. or tool_input.get("file_path")
  54. or tool_input.get("path")
  55. or ""
  56. )
  57. if not path:
  58. return 0
  59. path = os.path.abspath(path)
  60. if os.path.splitext(path)[1].lower() != ".php":
  61. return 0
  62. if not os.path.isfile(path):
  63. return 0
  64. try:
  65. text = open(path, encoding="utf-8", errors="ignore").read()
  66. except Exception:
  67. return 0
  68. hits = scan(text)
  69. if hits:
  70. print("[SECURITY-WARN] 发现安全红线隐患:")
  71. for h in hits:
  72. print(" - " + h)
  73. return 0
  74. if __name__ == "__main__":
  75. sys.exit(main())