三类扫描不可替代:SAST 看代码怎么写的,依赖扫描看引用了谁,密钥扫描看有没有把秘密写进去。 六个直接依赖带出十三个传递依赖,攻击面是声明数量的三倍多。 密钥扫描最该放在提交前——进了 git 历史,改代码也没用,得换密钥。
上一篇审的是自己写的代码。这一篇把范围扩到「你引用的代码」。 为什么代码审查不够? 因为出问题的地方可能根本不在你的 diff 里——它在一个你没看过、也没写过的依赖里。 Demo 造一个小仓库,注入三类问题:SQL 拼接、硬编码令牌、三个有漏洞的依赖版本。 三个扫描器各跑一遍,各命中各的。 再算一次供应链的攻击面。
一、三类扫描的分工
| 扫描器 | 看什么 | 典型命中 |
|---|---|---|
| SAST | 源码怎么写的 | SQL 拼接、命令注入、路径穿越 |
| 依赖扫描 | 引用了谁、哪个版本 | 已知漏洞的库版本 |
| 密钥扫描 | 有没有把秘密写进仓库 | 令牌、私钥、连接串 |
三类不可替代,因为它们的输入不一样。 SAST 读源码,依赖扫描读清单,密钥扫描读全部文本(包括配置文件)。 Demo 里三类各命中一处、一处、三处。 注意依赖扫描命中三处,因为那三个库的版本都刚好落在有洞的那一版上。 密钥扫描那一类的处理方式跟其他两类完全不同。 别的两类发现问题,改代码就行;密钥进了 git 历史,改代码没用—— 历史里还留着,任何能 clone 的人都能拿到。 所以它唯一的修法是换密钥,而换密钥意味着所有用到它的地方都要改。 这就是为什么密钥扫描要放在提交前。 放在提交后,你面对的不是「改一行」,是「走一遍密钥轮换流程」。
二、供应链风险
「我不用第三方库」这句话在今天基本不成立。 就算你一个都不装,你用的框架装了一堆。
| 层级 | 数量 | 你有多熟 |
|---|---|---|
| 直接依赖 | 6 | 写在清单里,大致知道 |
| 传递依赖 | 13 | 多半没看过 |
| 合计 | 19 | 每一个都是一次信任 |
Demo 里六个直接依赖带出十三个传递依赖,合计十九个,是声明数量的三倍多。 这个倍数不算高——真实项目里十倍很常见。 传递依赖的风险不在「它有害」,在「你不知道它存在」。 出问题的时候,你的清单里找不到它,所以第一反应是「不是我们的锅」。 然后排查两天,发现是你依赖的库依赖的那个库。 所以「锁版本」这件事的意义不只是「可重复构建」。 它还让「我到底用了什么」这个问题有一个确定的答案。 没有锁文件的时候,同一个清单在不同机器上装出来的传递依赖可能不一样。
三、演进方向
| 现在 | 在往哪走 |
|---|---|
| 提交后跑扫描 | 提交前跑,问题不进历史 |
| 扫源码文本 | 扫数据流,追污点从入口到汇点 |
| 人看告警 | 先按可利用性排序 |
| 审自己的代码 | 连依赖和构建脚本一起审 |
第二行的变化最大。
文本扫描只能告诉你「这里有个拼接」,数据流扫描能告诉你「用户输入能走到这里」。
后者才是「能不能被利用」的判据。
第三行更实际:告警多到没人看,所以「先看哪一条」比「报了多少条」重要。
一个百分之九十是噪音的扫描器,实际效果不如一个只报三条但条条要命的。
第四行是最容易被忽略的一行。
构建脚本和 CI 配置也是代码,而且它们通常有更高的权限——
一个被改过的 Makefile 比一个被改过的 .py 危险得多。
四、动手 Demo:三类扫描:源码、依赖、密钥,各管一段
一个小仓库:一个 SQL 拼接、一个硬编码令牌、六个依赖里三个有洞。 三个扫描器各跑一遍,看各命中什么。 扫描器都是二十行左右的规则——真实工具复杂得多,但「三类各管一段」这件事是一样的。
# SAST:找 SQL 字符串拼接
if re.search(r"\+\s*\w+\s*\+|", ln) and "SELECT" in ln:
print("SQL 字符串拼接", i)
# 密钥扫描:长串 + 变量名带 TOKEN/KEY/SECRET
if re.search(r"[\"'][A-Za-z0-9_\-]{20,}[\"']", ln) and \
re.search(r"TOKEN|KEY|SECRET", ln, re.I):
print("疑似硬编码令牌", i)
# 依赖扫描:比对已知有洞的版本表
if name in VULN and ver == VULN[name][0]:
print(name, ver, "->", VULN[name][1])
实跑输出:
--------------------------------------------------------------------
三类扫描:源码、依赖、密钥,各管一段
--------------------------------------------------------------------
三个扫描器分工明确,缺一类就有一类问题没人管。
① 被扫的仓库
--------------------------------------------------------------------
app/client.py(4 行)
app/query.py(5 行)
requirements.txt(6 行)
② 三类扫描各跑一遍
--------------------------------------------------------------------
| 扫描器 | 文件 | 行 | 命中 |
|---|---|---|---|
| SAST | app/query.py | 6 | SQL 字符串拼接 |
| 密钥扫描 | app/client.py | 2 | 疑似硬编码令牌 |
| 依赖扫描 | requirements.txt | — | requests 2.19.0 有洞,升到 2.20.0 |
| 依赖扫描 | requirements.txt | — | pyyaml 3.13 有洞,升到 5.1 |
| 依赖扫描 | requirements.txt | — | urllib3 1.23 有洞,升到 1.24.2 |
三类各命中 1 / 1 / 3 处。
! 三类不可替代:SAST 看「代码怎么写的」,依赖扫描看「引用了谁」,
密钥扫描看「有没有把秘密写进仓库」。
其中密钥扫描最该放在提交前——因为它一旦进了 git 历史,改掉代码也没用,得换密钥。
③ 供应链风险怎么估
--------------------------------------------------------------------
| 层级 | 数量 | 说明 |
|---|---:|---|
| 直接依赖 | 6 | 你写在 requirements 里的 |
| 传递依赖 | 13 | 依赖的依赖,你多半没看过 |
| 合计 | 19 | 每一个都是一次信任 |
六个直接依赖带出 13 个传递依赖,攻击面是声明数量的 3.2 倍。
! 这个倍数不是危言耸听,是「你信任了没看过的代码」的量化。
它不能让你不装依赖,但能让你在装第十个的时候多想一秒。
④ 演进方向
--------------------------------------------------------------------
| 现在 | 在往哪走 |
|---|---|
| 提交后跑扫描 | 提交前跑,问题不进历史 |
| 扫源码文本 | 扫数据流,追污点从入口到汇点 |
| 人看告警 | 先按可利用性排序,能利用的排前面 |
| 审自己的代码 | 连依赖和构建脚本一起审 |
第三行值得单说:安全告警的数量会多到没人看,所以「先看哪一条」比「报了多少条」更重要。
三张表一起看,能看出「分工」这两个字的意思。 SAST 命中一行代码,依赖扫描命中一个版本号,密钥扫描命中一个字符串——三种东西,三种修法。 依赖扫描那三行要看清:requests 2.19.0、pyyaml 3.13、urllib3 1.23。 三个都是当年的正常版本,只是后来被发现有洞—— 这就是为什么依赖扫描要持续跑,不是装的时候跑一次。 供应链那段:六带出十三,合计十九。 数字不大,但要看「你有多熟」那一列——十三那一行写的是「多半没看过」。 这不是危言耸听,这是事实描述。 最后那句「先看哪一条比报了多少条更重要」值得记。 安全工具的价值不在检出数量,在「能不能让人真的去修」。
30 秒实验
- 在你项目里搜一次
sk-、ghp_、AKIA开头的字符串。 搜到就说明密钥扫描该上了。 - 数一下你的锁文件里有多少个包。 跟你的清单条数比一比,那个倍数就是你的攻击面。
- 查一下你构建脚本最近的改动记录。 它是代码,而且权限比普通代码高。
常见误区
- 「我们自己写的代码没问题,不用扫。」 问题可能不在你的代码里,在你依赖的库里。 而那个库你多半没看过。
- 「密钥泄露了改掉代码就行。」 它已经在 git 历史里了。 唯一的修法是换密钥,而换密钥要走一遍轮换流程。
- 「扫描器报得多说明覆盖全。」 报得多说明噪音大。 一个没人看的扫描器,等于没有。
自测三问(点开对答案)
1. 三类扫描的输入有什么不同? —— **SAST 读源码,依赖扫描读清单,密钥扫描读全部文本。** **输入不一样,所以不可替代。** 2. 为什么密钥扫描要放在提交前? —— **因为进了 git 历史,改代码没用。** **修法从「改一行」变成「走一遍密钥轮换」。** 3. 传递依赖的风险核心是什么? —— **不是「它有害」,是「你不知道它存在」。** **出事时你在自己清单里找不到它。**最后一篇收束整章:四级风险分级,以及信任是怎么一级一级攒出来的。
- ☐ (本篇完,下一篇 12.5 自动化的边界:风险分级与信任建立)