Web 安全代码审计与漏洞防御学习笔记
学习目标: 从代码审计、安全开发和漏洞防御角度理解常见 Web 漏洞的形成原因,建立
Source → Data Flow → Validation → Sink的统一安全分析思维。适用范围: 本笔记仅用于本地实验、安全自查、代码修复和漏洞防御研究,不包含攻击利用载荷、数据库数据提取、自动化攻击或未授权安全测试方法。
1. Web 漏洞统一分析模型
很多 Web 漏洞表面上完全不同,例如:
- SQL 注入
- 文件上传
- 代码执行
- 反序列化
- XSS
- 路径遍历
- SSRF
- XXE
- 模板注入
但是从代码审计角度,它们通常都可以抽象成类似的数据流:
| |
最核心的问题是:
用户可控的数据,有没有跨越“数据边界”,进入本应由程序控制的解释器、文件系统或执行环境。
1.1 Source:用户可控输入
Source 指可能受到用户或外部环境影响的数据来源。
常见 Source:
| |
PHP 示例:
| |
Python Flask 示例:
| |
两种情况下:
| |
需要特别注意:
来自数据库的数据也不一定可信。
例如:
| |
因此不能简单认为:
| |
1.2 Sink:危险数据使用位置
Sink 指程序最终把数据交给某个具有解释、执行或敏感操作能力的位置。
SQL Sink
常见:
| |
系统命令 Sink
常见:
| |
动态代码执行 Sink
常见:
| |
文件 Sink
常见:
| |
反序列化 Sink
常见:
| |
浏览器输出 Sink
常见:
| |
看到这些函数时不能直接判断:
| |
真正需要继续分析的是:
| |
1.3 Source → Sink 数据流模型
源码审计时不要只搜索函数名。
应该完整追踪:
| |
例如:
| |
数据流:
| |
其中:
| |
真正的问题发生在:
| |
1.4 数据与代码边界
安全代码最重要的目标之一:
| |
危险模式:
| |
正确模式:
| |
以后学习任何 Web 漏洞,都可以先问:
这段用户输入最后被谁解释?
2. SQL 注入缺陷
2.1 漏洞根源
SQL 注入的根本问题不是:
用户输入了某个特殊字符。
真正的问题是:
用户可控数据进入了 SQL 结构,数据库无法继续可靠地区分“SQL 程序结构”和“普通参数数据”。
错误数据流:
| |
正确数据流:
| |
2.2 错误案例一:PHP 字符串拼接 SQL
错误代码
| |
逐行分析
第一行:
| |
$_GET 是用户可控数据。
因此:
| |
第二行:
| |
这里发生:
| |
最终得到:
| |
数据与 SQL 代码之间的边界已经被破坏。
第三行:
| |
数据库开始解析整个字符串。
安全修复
| |
修复后:
| |
数据库驱动负责把:
| |
绑定为:
| |
2.3 错误案例二:Python f-string SQL
错误代码
| |
数据流:
| |
安全修复
SQLite 示例:
| |
这里有两层保护:
| |
注意:
类型验证不能代替 SQL 参数化。
类型验证解决的是:
| |
参数化解决的是:
| |
2.4 错误案例三:动态 ORDER BY
错误代码
| |
问题在于:
| |
最终成为:
| |
例如列名。
普通 SQL 参数绑定主要用于:
| |
而不是:
| |
因此不能简单依赖:
| |
实现任意动态列名。
安全修复
| |
数据流:
| |
这里进入 SQL 的:
| |
而是:
| |
2.5 为什么黑名单防 SQL 不可靠
例如开发人员尝试:
| |
这种思路的问题:
SQL 是一门语言,而不是一张危险字符表。
同一种逻辑可能具有:
| |
所以正确问题不是:
用户有没有输入“坏字符串”?
而应该是:
用户数据有没有机会成为 SQL 结构的一部分?
2.6 SQL 防御原则
推荐优先级:
| |
可以记成:
| |
3. 文件上传风险
3.1 漏洞根源
文件上传问题本质上是:
| |
需要同时关注:
| |
3.2 错误案例一:直接使用原始文件名
错误代码
| |
问题:
| |
由客户端提供。
服务器直接拿:
| |
作为:
| |
同时:
| |
如果位于 WebRoot 中,上传内容还可能被 Web 服务器直接访问。
安全修复
| |
这里使用:
| |
3.3 错误案例二:只相信 Content-Type
错误代码
| |
问题一:
| |
来自 HTTP 请求。
它本质是:
| |
不能把它当作服务器安全证明。
问题二:
| |
通常可以直接通过 Web 访问。
问题三:
| |
也是客户端提供的原始文件名。
安全修复
| |
3.4 错误案例三:只过滤文件名
错误代码
| |
开发者可能认为:
| |
但实际上它没有解决:
| |
安全修复思路
原则:
| |
例如:
| |
服务器实际保存:
| |
数据库单独记录:
| |
下载时:
| |
而不是:
| |
3.5 文件上传防御原则
推荐:
| |
不能单独依赖:
| |
4. 代码执行风险
4.1 漏洞根源
代码执行类问题本质:
| |
这里的解释器可能包括:
| |
4.2 错误案例一:PHP eval
错误代码
| |
数据流:
| |
这里用户数据直接进入:
| |
安全修复
假设业务只是一个简单计算器:
| |
现在数据流:
| |
用户不再提供:
| |
4.3 错误案例二:Python shell=True
错误代码
| |
数据流:
| |
Shell 会对整个字符串再次解释。
安全修复
| |
安全措施:
| |
4.4 错误案例三:PHP system
错误代码
| |
问题:
| |
安全修复思路
最优解决方案通常不是:
| |
而是:
| |
例如:
| |
只有确实必须启动外部程序时,才考虑:
| |
4.5 代码执行防御原则
推荐优先级:
| |
越往下:
| |
代码审计时重点关注:
| |
然后继续确认:
| |
5. 反序列化风险
5.1 什么是序列化
序列化:
| |
反序列化:
| |
JSON:
| |
主要表达:
| |
而某些语言原生对象序列化格式可能包含:
| |
所以:
| |
属于高风险数据边界。
5.2 错误案例一:PHP unserialize
错误代码
| |
数据流:
| |
客户端数据进入:
| |
安全修复
| |
更加推荐的 Session 模型:
| |
数据实际保存在:
| |
5.3 错误案例二:Python pickle
错误代码
| |
问题:
| |
客户端控制的数据直接进入:
| |
安全修复
| |
JSON 解析之后还要继续:
| |
例如:
| |
5.4 错误案例三:不安全 YAML
错误代码
| |
问题:
| |
安全修复
如果业务确实需要 YAML:
| |
如果业务只需要普通数据:
| |
原则:
输入语言越复杂,需要处理的安全语义通常越多。
5.5 反序列化防御原则
推荐:
| |
6. XSS 跨站脚本风险
6.1 漏洞根源
XSS 的核心安全边界:
| |
问题发生在:
| |
被浏览器解释成:
| |
而不是:
| |
6.2 错误案例一:PHP 直接输出
错误代码
| |
数据流:
| |
安全修复
| |
数据流变成:
| |
6.3 错误案例二:Jinja 关闭自动转义
错误模板
| |
safe 的含义是:
| |
如果:
| |
来自用户,就会破坏重要的输出安全边界。
安全修复
| |
原则:
| |
重点关注:
| |
6.4 错误案例三:富文本直接保存并输出
错误代码
| |
随后模板:
| |
数据流:
| |
这种类型叫:
| |
修复方案一:业务不需要 HTML
只保存:
| |
模板:
| |
修复方案二:业务确实需要富文本
使用成熟的 HTML Sanitizer:
| |
不能简单:
| |
因为:
| |
而不是:
| |
6.5 输出上下文非常重要
下面这些上下文:
| |
安全处理方式并不完全一样。
因此不能理解为:
| |
需要判断:
| |
6.6 XSS 防御原则
推荐:
| |
7. 五类漏洞的统一理解
| 漏洞 | 用户数据最终进入 | 根本问题 | 核心防御 |
|---|---|---|---|
| SQL 注入 | SQL Parser | 数据进入 SQL 结构 | 参数化查询 |
| 文件上传 | 文件系统 / Web服务器 | 用户控制危险文件或路径 | 校验 + 隔离 |
| 代码执行 | Shell / 代码解释器 | 数据变成命令或代码 | 避免动态执行 |
| 反序列化 | Object Deserializer | 不可信数据恢复复杂对象 | JSON + Schema |
| XSS | Browser Parser | 数据变成 HTML/JS 结构 | 上下文编码 |
统一模型:
| |
所以源码审计最重要的问题不是:
这个漏洞叫什么名字?
而是:
用户数据最后被谁解释?
8. 现代 WAF 检测原理
现代 WAF 通常不是简单使用几个正则表达式。
典型检测流程可以抽象成:
| |
8.1 特征匹配
WAF 可以检查:
| |
优势
| |
短板
最大的理论问题:
| |
因此会出现:
| |
以及:
| |
例如:
| |
本身是正常业务,却可能包含大量:
| |
8.2 语义分析
更加先进的检测可能先把输入拆成:
| |
例如数据库相关输入,不只是搜索:
| |
而是分析:
| |
优势
可以更好地处理:
| |
一般比:
| |
更强。
短板
存在一个重要理论问题:
| |
后端可能使用:
| |
这些数据库之间:
| |
存在差异。
因此可能出现:
| |
即:
| |
8.3 行为识别
安全系统还可以观察多个请求之间的关系。
例如:
| |
正常用户:
| |
异常自动化行为可能表现:
| |
优势
可以发现:
| |
短板
正常业务也可能产生自动化行为:
| |
所以:
| |
8.4 异常评分
现代 WAF 经常使用:
| |
不是简单:
| |
而是类似:
| |
超过某个阈值后:
| |
这种模式的优点:
| |
降低对单个规则的依赖。
8.5 WAF 的正确定位
错误理解:
| |
正确结构:
| |
即:
| |
所以:
| |
而不是:
| |
9. Web 应用安全自检流程
完整安全自检流程:
| |
9.1 第一阶段:资产梳理
首先整理应用所有输入入口:
| |
然后整理技术栈:
| |
最终得到:
| |
9.2 第二阶段:源码审计
SQL Sink
搜索:
| |
然后检查附近有没有:
| |
核心问题:
| |
文件 Sink
搜索:
| |
检查:
| |
代码执行 Sink
PHP:
| |
Python:
| |
发现后继续:
| |
反序列化 Sink
PHP:
| |
Python:
| |
继续问:
| |
XSS Sink
重点搜索:
| |
然后检查:
| |
9.3 第三阶段:黑盒防御验证
这里的目标:
| |
而不是:
| |
SQL 自查
可以测试:
| |
例如合法姓名:
| |
可以检查程序是否:
| |
如果普通合法输入都能触发 SQL 错误:
| |
文件上传自查
测试:
| |
检查:
| |
XSS 自查
可以使用完全无害的标记:
| |
观察页面是否:
| |
而不是:
| |
无需执行任何 JavaScript。
代码执行自查
检查:
| |
不需要真正执行系统命令。
反序列化自查
检查接口有没有接受:
| |
如果存在:
| |
9.4 第四阶段:权限与部署检查
检查:
| |
9.5 第五阶段:上线前安全检查
| |
10. 本地 Web 安全实验环境
10.1 推荐架构
推荐学习环境:
| |
Web 实验结构:
| |
推荐组件:
| |
10.2 Docker 网络原则
建立单独实验网络:
| |
Web 服务只暴露给本机:
| |
数据库:
| |
尽量避免实验数据库直接绑定:
| |
网络结构:
| |
10.3 日志设计
实验环境建议记录:
| |
不要直接记录:
| |
10.4 Request ID
给每个 HTTP 请求生成:
| |
例如:
| |
以后研究:
| |
会非常方便。
10.5 正确实验方式
第一步:构建本地错误代码
只在自己的实验环境中创建:
| |
目的是:
| |
第二步:记录正常行为
观察:
| |
第三步:修改安全代码
例如:
| |
或:
| |
第四步:重复相同测试
重新输入:
| |
第五步:比较修复前后
| |
真正需要理解:
为什么修复以后,用户数据无法再跨越数据与代码的边界?
11. 开发人员最容易踩的 8 个安全误区
11.1 过滤特殊字符就能防 SQL 注入
错误:
| |
正确:
| |
过滤解决:
| |
参数化解决:
| |
11.2 文件扩展名正确就是安全文件
错误。
文件上传还需要考虑:
| |
正确:
| |
11.3 Shell 参数经过 replace 就安全
错误。
正确优先级:
| |
11.4 数据库中的数据一定可信
错误。
可能存在:
| |
因此:
| |
需要根据:
| |
重新判断安全性。
11.5 使用 ORM 就绝对安全
ORM 的标准接口通常会:
| |
但是需要特别关注:
| |
因此:
| |
而不是:
| |
11.6 CSP 可以完全解决 XSS
错误。
CSP 更适合定位为:
| |
XSS 的核心防线仍然是:
| |
11.7 WAF 没报警就没有漏洞
错误。
WAF 很难完全理解:
| |
所以:
| |
11.8 内部系统不需要安全设计
错误。
内部系统通常具有:
| |
因此同样应该遵守:
| |
即:
| |
12. 长期可持续的安全开发规范
12.1 安全开发生命周期
推荐:
| |
安全不能只发生在:
| |
而应该贯穿:
| |
12.2 Code Review 三问
每发现一个危险 Sink,都问下面三个问题。
第一问:输入来自哪里?
| |
例如:
| |
第二问:经过了什么安全处理?
例如:
| |
第三问:为什么必须使用这个危险 API?
例如发现:
| |
先问:
| |
很多时候:
| |
比:
| |
更加可靠。
12.3 Source → Sink 审计方法
固定流程:
| |
13. PHP 安全代码模板
下面是一套适合作为学习参考的 PHP 安全代码骨架。
| |
14. Python Flask 安全代码模板
下面是一套经过整理的 Python Flask 示例骨架。
| |
15. Web 源码审计速查表
| 看到的代码 | 第一反应 |
|---|---|
| SQL + 用户变量 | 是否使用参数化? |
| f-string SQL | 用户数据是否进入 SQL 结构? |
| 动态 ORDER BY | 是否严格 allowlist? |
| 动态表名/列名 | 是否通过程序白名单映射? |
| 文件名 + 用户输入 | 是否存在路径或上传风险? |
| 上传到 static/www | Web 是否能够直接访问? |
eval() | 为什么需要动态执行? |
exec() | 用户数据是否能够到达? |
system() | 为什么必须使用 Shell? |
shell=True | 是否可以改为 shell=False? |
unserialize() | 数据是否来自外部? |
pickle.loads() | 为什么不用 JSON? |
yaml.load() | Loader 是否安全? |
safe/raw | 为什么关闭自动转义? |
| HTML + 用户变量 | 是否根据上下文编码? |
innerHTML | 数据是否可信? |
大量 replace() | 是否在用黑名单代替安全 API? |
| 数据库高权限账户 | 能否降低权限? |
| Debug 开启 | 是否可能泄漏敏感信息? |
| Secret 写进源码 | 是否应该移动到安全配置? |
16. 最终安全模型
整个 Web 安全开发可以压缩成:
| |
最重要的一句话:
安全开发的目标不是识别所有恶意字符串,而是从程序结构上保证:无论用户输入什么,它都无法从“数据”变成“代码”。
17. 推荐源码审计思维
以后拿到一个 PHP 或 Python Web 项目,可以按照下面顺序审计。
17.1 第一步:找 Source
| |
先确认:
| |
17.2 第二步:追踪 Data Flow
| |
不要只看:
| |
还要理解:
| |
17.3 第三步:寻找 Sink
重点关注:
| |
17.4 第四步:检查 Validation
问:
| |
注意:
| |
不是单纯:
| |
17.5 第五步:判断数据边界
问:
用户是否能够改变程序原本应该执行的语义?
例如本来:
| |
如果输入能够影响:
| |
那么边界出现问题。
本来:
| |
如果文件最终可以被:
| |
边界也出现问题。
17.6 第六步:进行安全修复
不要优先思考:
| |
而应该优先思考:
| |
例如:
| |
17.7 最终审计公式
最终记住:
| |
对应四个问题:
| |
这套模型可以继续扩展到:
| |
18. 全文总结
Web 安全源码审计最重要的思维并不是:
| |
也不是:
| |
而是:
| |
以后看到任何 Web 代码,可以先问五个问题:
- 数据从哪里来?
- 用户可以控制多少?
- 数据中间经过了哪些处理?
- 数据最终进入哪个 Sink?
- 程序如何保证数据永远只是数据,而不会成为代码?
如果这五个问题能够逐渐回答清楚,就已经开始真正建立 Web 源码审计与漏洞防御 的分析思维。