安全的代码,安全的系统 — 左移安全与供应链防线
安全的代码,安全的系统 — 左移安全与供应链防线
这一章讲什么: 一个漏洞在开发阶段修要几分钟,上线后再修可能贵 100 倍还可能被黑客抢先利用—— 所以安全检查必须「左移」到写代码的那一刻。本章把左移落地成一条五阶段流水线, 再逐层讲透防线:自己的代码(SAST/DAST)、别人的代码(依赖扫描)、运行环境(IaC 扫描), 以及让这一切真正起效的人(安全意识与安全冠军)。 本章含大量可直接照抄的代码与命令。
1. 主线例子:一个登录框的四次过堂
本章的主线例子,是一个用户输入从写代码到上线被检查的全过程。
假设你写了一个登录接口,用户名的值会被拼进 SQL 查询。我们跟着这个输入走四道检查:
第 1 道(IDE 里,写代码那一刻):
你写下 query = f"SELECT * FROM users WHERE username = '{username}'"
IDE 插件当场画波浪线:「字符串拼接 SQL,可被注入」
第 2 道(提交时,SAST):
SonarQube 扫源码,flag 同一行;CI 流水线把它当质量门,高危不合并
第 3 道(测试环境,DAST):
OWASP ZAP 对运行中的应用发攻击包:username=admin' OR '1'='1
如果应用真放行了,报告「SQL 注入,严重」
第 4 道(运行时,监控):
生产流量出现异常模式,运行时安全工具告警
图说:同一个漏洞,被四道独立的机制先后拦住——任何一道漏了,后面还有。
四道的具体工具与顺序沿原书 DevSecOps 流水线;攻击字符串 ' OR '1'='1 是教科书经典负载,原书原样使用。这四道就是本章的骨架:§2 讲为什么必须这么早(shift-left),§3 讲第 1、2 道(代码层的漏洞长什么样), §4 讲依赖这道最容易被忽视的防线,§5 把 SAST 和 DAST 这对互补机制讲透,§6 是工具分工与真实案例, §7 回答「工具都有了,为什么还出事」——因为没人当真。
2. Shift-left:为什么安全必须挪到最左边
SDLC(软件开发生命周期:需求→设计→开发→测试→部署)画在纸上是从左到右的一条线。 「左移」就是把安全检查从线的最右端(发布前)挪到左边(设计和写代码的时候)1。
旧模型的问题,原书用一句话概括:生产环境修一个问题,可能比设计阶段修贵 100 倍; 更要命的是,部署之后、补丁之前,漏洞就暴露在攻击者面前2。四个逼你左移的因素: 攻击面变大(微服务、API、容器,入口多到数不过来)、DevOps 的发布速度(每周甚至每天发, 人工终审根本排不进日程)、监管(GDPR、HIPAA、PCI DSS 这些法规缩写,涉及欧盟数据保护、 美国医疗信息、支付卡行业,违规代价极高)、成本(越早发现越便宜)3。
传统模型与左移模型的对照(原书表 4.1)4:
| 维度 | 传统模型 | 左移模型 |
|---|---|---|
| 检查时机 | SDLC 末端、发布前 | 嵌入每一阶段 |
| 修复成本 | 极高(生产补丁、停机) | 低(写代码/设计时就地解决) |
| 发布速度 | 被晚期漏洞拖慢 | 因主动预防而加快 |
| 开发者参与 | 极少,「安全是别人的事」 | 高,第一天起归开发者 |
| 被利用风险 | 高(部署后才发现) | 低(攻击者下手前已缓解) |
反直觉的一句要划重点:安全和速度不是对立目标——安全的代码过流水线反而更快, 因为没有晚期返工和热修复5。
3. 代码层的漏洞长什么样:SQL 注入与它的修复
本章的承重例子是 SQL 注入(SQL Injection):把用户输入直接拼进 SQL(数据库查询语言)语句, 输入就不再是「数据」而会被数据库当成「命令」执行。原书给了不安全和安全两版代码(照抄结构):
# 不安全:字符串拼接
query = f"SELECT * FROM users WHERE username = '{username}';"
cursor.execute(query)
# 安全:参数化查询——输入永远只被当作数据
query = "SELECT * FROM users WHERE username = ?;"
cursor.execute(query, (username,))区别就在一处:参数化查询把「语句」和「数据」分成两次传递, 用户输入无论写什么都不可能再改变语句的结构6。
DevSecOps 流水线(在 CI/CD 的每一步嵌一道安全检查)的五阶段版本7:
| 阶段 | 嵌入的检查 |
|---|---|
| 写代码 | IDE 里跑 lint 和 SAST |
| 构建 | 依赖扫描,查已知 CVE(公共漏洞编号) |
| 测试 | DAST 对运行中的服务模拟攻击 |
| 部署 | IaC 模板查配置错误 |
| 运行 | 运行时安全工具监控生产流量异常 |
配套五条最佳实践:开发者先培训(OWASP Top 10,即业界最常见的十类 Web 漏洞榜单)、 能自动化的全自动化、工具嵌进 IDE(问题当场看到,而不是几周后)、安全策略也写成代码进版本库、 反馈要快到以 PR 评论的形式出现8。
4. 供应链防线:你的代码里没有的漏洞
4.1 问题:你的依赖的依赖的依赖
现代应用几乎不从零写起:npm、pip、Maven 这类包管理器让几行配置就能引入一个库。
但每加一个依赖就多一层风险——库还有自己的依赖(叫传递依赖,
transitive dependency),一个小项目不知不觉就背了几百个包,很多包你都不知道自己在用。
这就是「软件供应链安全」问题的由来9。
原书给的两个真实事故,都值得记住:
- Log4Shell(2021 年底):Java 日志库 Log4j 有一个严重的远程代码执行漏洞 (RCE:攻击者通过让目标执行自己提交的代码来接管系统)。Log4j buried 在无数系统的 传递依赖深处,很多公司直到漏洞扫描器扫出来,才知道自己在用 Log4j10;
- 拼写仿冒(typosquatting):攻击者在包仓库发布名字几乎一样的恶意包
(原书例子:
reqeusts冒充requests),开发者一个手滑就把恶意软件装进了系统11。
风险四类:未打补丁的已知漏洞、传递依赖的隐藏风险、恶意包、许可证合规12。
4.2 一个具体项目的解剖
原书拿了一个只有三行依赖的 Python 项目开刀(照抄):
requirements.txt:
Flask==1.1.1
requests==2.19.1
numpy==1.18.0看着人畜无害。实际:requests 2.19.1 有一个会导致凭证泄露的已知漏洞;
Flask 1.1.1 带着过时的传递依赖;NumPy 落后好几个版本13。
漏洞库里(NVD,美国国家漏洞数据库)这些都编了号、查了就能对上。
4.3 工具与 Trivy 实战
依赖扫描工具对照(原书表 4.2,名字全留)14:
| 工具 | 生态 | 干什么 |
|---|---|---|
| Trivy | 多语言 + Docker + IaC | 扫依赖、容器镜像、文件系统、IaC,一条命令全覆盖 |
| Sonatype OSS Index | 多语言 | 免费 API,对 CVE 查开源包 |
| npm audit | Node.js | 查 package.json / package-lock.json |
| pip-audit | Python | 对 Python 漏洞库扫 requirements.txt |
| OWASP Dependency-Check | 多语言 | 老牌开源工具,Java/.NET 强 |
Trivy 的实战输出(照抄结构),扫的就是上面那个三行项目15:
$ trivy fs --security-checks vuln .
requirements.txt (pip)
Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 1, CRITICAL: 0)
│ Library │ Severity │ Installed │ Fixed In │
│ requests │ HIGH │ 2.19.1 │ 2.20.0 │
│ flask │ MEDIUM │ 1.1.1 │ 1.1.2 │注意最后一列 Fixed In:它直接告诉你升级到哪个版本就安全——反馈是可以行动的,
而不是只报忧。嵌进 CI/CD 时 Trivy 常作为强制阶段:扫出高危或严重漏洞就阻断部署16。
卫生文化三条:Dependabot/Renovate 这类服务自动发「依赖该升级了」的 PR; 盯住传递依赖(工具能画出依赖树);定好可信包源与许可证政策17。
5. SAST 与 DAST:一静一动,互为补集
这是本章的机制核心。两个缩写必须分清:
- SAST(静态应用安全测试):不运行程序,直接分析源代码/字节码/二进制, 找不安全的写法和模式;
- DAST(动态应用安全测试):把应用跑起来,从外部模拟攻击, 也叫黑盒测试(不看内部,只看外部行为)18。
对照表(原书表 4.3)19:
| 维度 | SAST | DAST |
|---|---|---|
| 执行方式 | 不运行代码,看文本 | 运行中的应用,打真实负载 |
| 生命周期位置 | 早(写代码/构建时) | 晚(部署到测试环境后) |
| 需要什么 | 源码、字节码或二进制 | 不需要源码,只要应用在线 |
| 强项 | 尽早发现编码缺陷,嵌入 CI/CD | 发现运行时问题:认证缺陷、XSS |
| 弱项 | 误报多;看不到运行时环境问题 | 看不到代码深处的漏洞 |
SAST 的承重例子:命令注入。 一个允许用户 ping 服务器的功能,把用户输入拼进系统命令20:
String cmd = "ping " + userInput;
Runtime.getRuntime().exec(cmd); // 危险:用户控制了一条系统命令如果用户输入 127.0.0.1 && rm -rf /,程序会先 ping,再把整个文件系统删掉。
SAST 工具扫到 exec 拼接就告警。修复:用正则白名单校验输入(只允许字母数字和点),
并用 ProcessBuilder 把命令和参数分开传,而不是拼成一个字符串21。
DAST 的承重例子:同一个登录框。 OWASP ZAP 对运行中的登录表单发送
username=admin' OR '1'='1&password=anything——如果应用真的放行了,
ZAP 报告 SQL 注入。SAST 看到的是代码里那行拼接;DAST 看到的是「系统在攻击下的行为」,
两者抓的漏洞集合不重叠22。
组合用法:真实流水线里 SAST 跑在提交/构建阶段,DAST 跑在预发阶段,前后接力23。
6. 工具生态与四个案例
原书把安全自动化工具按栈层分类:依赖扫描、SAST、DAST、容器镜像扫描、IaC 扫描—— 不是互斥选项,是纵深防御的分层24。四种主力工具,各给一节特写:
| 工具 | 主战场 | 书里点的能力 |
|---|---|---|
| Trivy | 依赖 + 容器 + IaC | trivy image myapp:latest 扫镜像;trivy config ./terraform 扫 IaC |
| SonarQube | SAST | 硬编码凭证、注入风险、缺输入校验、死代码;当质量门,高危阻断构建 |
| OWASP ZAP | DAST | zap-cli quick-scan 在 CI 里全自动打运行中的应用 |
| Checkov | IaC 扫描 | 扫 Terraform/K8s 清单,如抓到 acl = "public-read" 的 S3 桶(对全世界公开可读)25 |
四个落地案例给了量级数字(都无出处,当示意读):金融科技初创把 Trivy 嵌进构建, 三个月内依赖相关漏洞降近 70%;电商平台把 ZAP 接进每夜测试,一次扫出一个 可篡改商品定价的认证绕过漏洞;SaaS 厂商用 Checkov 做策略即代码, 配置错误类事故降 80%26。
共性难题也要记住:开发者嫌扫描慢、嫌烦;结果被误报淹没;调规则、接流水线比预想费时。 原书的结论是:自动化要配上文化对齐和开发者 ownership 才成功——这直接引出第 7 节27。
7. 工具之外的人:意识、冠军与度量
原书这一节的立场很硬:工具自己会失效——开发者把告警当误报关掉、嫌流程碍事绕过它—— 漏洞的根子常是人的疏忽(复用凭证、无视警告、没理解风险)28。
落到日常三个机制:
- 检查贴着动作做:IDE 插件在代码提交前就划波浪线;Git 钩子(pre-commit 检查) 硬拦硬编码的密钥;反馈越靠近出错点,越容易被当成帮助而不是 obstruction;
- 安全冠军(security champion):每个小队提名一名开发者,接受额外的安全编码与威胁建模训练, 当队里的第一联络人——把抽象风险翻译成队友能执行的动作;
- CTF 等实战演练:在受控环境里模拟真实攻击,让开发者亲眼看着漏洞怎么被利用29。
硬编码(把本该是配置的东西写死在代码里)密钥的例子一针见血(照抄对照)30:
API_KEY = "abcd1234-secret" # 危险:密钥进了源码库,等于公开
API_KEY = os.getenv("API_KEY") # 安全:从环境变量(运行时从系统环境按名字取的值)/密钥管理服务(AWS Secrets Manager、HashiCorp Vault)取工具会 flag 第一行,但没有意识的人会把它当误报点掉;有意识的人才知道为什么危险。
意识计划的度量(原书表 4.5,标明了是趋势观察而非行业基准):平均修复时长 14 天 → 3 天、 重复漏洞率 40% → 10%、安全评审参与率 25% → 80%31。案例:一家中型 SaaS 公司推安全冠军 计划,六个月重复漏洞降 50%、平均修复时间改善 30%,并从容通过了一次合规审计32。
判断(我们的,不是书里的): 本章最强的一个洞察藏在角落里:SAST 的误报问题 和「意识计划」其实是同一个问题的两面——误报率高的工具遇上没有安全判断力的团队, 唯一的稳定态就是告警被整体忽略;反过来,有经验的开发者会把误报当免费培训材料。 所以采购顺序应该是先培训后上工具,或者至少并行;先上工具后补意识, 大概率收获一屋子被静音的告警。 如果错,会错在: 新一代 AI 辅助的 SAST 误报率大降之后,「先训后器」的紧迫性会下降; 但「工具的输出需要有人解读、有人负责」这一层不会因为误报率下降而消失。
8. 边界与局限
- 威胁建模被一笔带过——只在左移清单里出现一次,没有任何展开;这是全书安全的最大缺口之一。
- 案例数字全部无出处(-70%、80%、50%、30%……),引用时注意是示意量级。
- 密钥管理只给了两行:环境变量和 Secrets Manager/Vault,轮换、审计、最小权限这些 生产实践没展开。
- 没有讲扫描工具自身的成本与延迟——流水线里每加一道门都是有账的,本书默认门越多越好, 没有讨论门的预算。
- AI 生成代码的安全风险在第 01 章点了名,本章完全没有接住(代码评审责任、AI 引入的依赖风险)。
9. 可带走的
- 漏洞越晚发现越贵:生产修复 ≈ 设计阶段修复成本的 100 倍(原书口径),而且补丁出来前就在被打;
- 左移 = 安全检查嵌进每个阶段,不是加一道末端闸门;
- SQL 注入的根因是「数据和命令拼在一起」;参数化查询从机制上根除它;
- 依赖是最大暗面:传递依赖 + 拼写仿冒;Log4Shell 的教训是「你不知道自己用了什么」;
- Trivy 一条命令扫依赖/镜像/IaC,
Fixed In列直接给修复版本; - SAST 看代码不看行为(早、误报多),DAST 看行为不看代码(晚、抓运行时);组合使用;
- 高危漏洞当质量门:扫描不通过,构建就停;
- 工具之上是人:告警被当误报忽略是安全计划的默认死法;安全冠军机制有量化收益(重复漏洞 -50%)。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 100 倍成本、左移定义 | Introduction(第 4 章) | text/22-fm-introduction.txt:61(搜「100 times」) · text/22-fm-introduction.txt:70(搜「shift-left」) |
| 表 4.1 传统 vs 左移 | Introduction(第 4 章) | text/22-fm-introduction.txt:122(搜「Dimension」) · text/22-fm-introduction.txt:230(搜「Table 4.1」) |
| SQL 注入两版代码 | Introduction(第 4 章) | text/22-fm-introduction.txt:242(搜「sqlite3」) · text/22-fm-introduction.txt:281(搜「parameterized query」) |
| 五阶段 DevSecOps 流水线 | Introduction(第 4 章) | text/22-fm-introduction.txt:299(搜「Code phase」) · text/22-fm-introduction.txt:319(搜「Figure 4.1」) |
| Log4Shell 与拼写仿冒 | Introduction(第 4 章) | text/22-fm-introduction.txt:359(搜「Log4Shell」) · text/22-fm-introduction.txt:362(搜「typosquatting」) |
| requirements.txt 解剖 | Introduction(第 4 章) | text/22-fm-introduction.txt:388(搜「Flask==1.1.1」) |
| Trivy 输出与 CI 集成 | Introduction(第 4 章) | text/22-fm-introduction.txt:544(搜「trivy fs」) · text/22-fm-introduction.txt:586(搜「mandatory stage」) |
| SAST/DAST 定义与对照表 | Introduction(第 4 章) | text/22-fm-introduction.txt:607(搜「without executing」) · text/22-fm-introduction.txt:754(搜「Table 4.3」) |
| 命令注入与 ZAP 实战 | Introduction(第 4 章) | text/22-fm-introduction.txt:766(搜「runCommand」) · text/22-fm-introduction.txt:847(搜「OR '1'='1」) |
| 工具特写与案例 | Introduction(第 4 章) | text/22-fm-introduction.txt:1180(搜「myapp:latest」) · text/22-fm-introduction.txt:1359(搜「finTech」) · text/22-fm-introduction.txt:1365(搜「80% reduction」) |
| 意识、冠军、硬编码密钥 | Introduction(第 4 章) | text/22-fm-introduction.txt:1409(搜「IDE plugins」) · text/22-fm-introduction.txt:1430(搜「Secret Scanning」) · text/22-fm-introduction.txt:1617(搜「50% reduction」) |
| 意识度量表 | Introduction(第 4 章) | text/22-fm-introduction.txt:1529(搜「14 days」) · text/22-fm-introduction.txt:1596(搜「Table 4.5」) |
Footnotes
-
出处:「Introduction」(第 4 章)第 70 段(
text/22-fm-introduction.txt:70,搜「left-to-right」)。 ↩ -
出处:「Introduction」(第 4 章)第 61 段(
text/22-fm-introduction.txt:61,搜「100 times」)。 ↩ -
出处:「Introduction」(第 4 章)第 90–103 段(
text/22-fm-introduction.txt:94,搜「Increased attack surface」;text/22-fm-introduction.txt:100,搜「GDPR」;text/22-fm-introduction.txt:103,搜「Cost efficiency」)。 ↩ -
出处:「Introduction」(第 4 章)第 122–230 段(
text/22-fm-introduction.txt:230,搜「Table 4.1」)。 ↩ -
出处:「Introduction」(第 4 章)第 107 段(
text/22-fm-introduction.txt:107,搜「enabler of agility」)。 ↩ -
出处:「Introduction」(第 4 章)第 242–290 段(
text/22-fm-introduction.txt:254,搜「Vulnerable to SQL Injection」;text/22-fm-introduction.txt:287,搜「cursor.execute(query, (username,))」)。 ↩ -
出处:「Introduction」(第 4 章)第 296–311 段(
text/22-fm-introduction.txt:299,搜「Code phase」)。 ↩ -
出处:「Introduction」(第 4 章)第 327–343 段(
text/22-fm-introduction.txt:331,搜「OWASP Top 10」;text/22-fm-introduction.txt:343,搜「pull request comments」)。 ↩ -
出处:「Introduction」(第 4 章)第 350–353 段(
text/22-fm-introduction.txt:353,搜「transitive dependencies」)。 ↩ -
出处:「Introduction」(第 4 章)第 359 段(
text/22-fm-introduction.txt:359,搜「Log4Shell」)。 ↩ -
出处:「Introduction」(第 4 章)第 362 段(
text/22-fm-introduction.txt:362,搜「typosquatting」)。 ↩ -
出处:「Introduction」(第 4 章)第 365–378 段(
text/22-fm-introduction.txt:369,搜「Unpatched vulnerabilities」;text/22-fm-introduction.txt:378,搜「License risks」)。 ↩ -
出处:「Introduction」(第 4 章)第 385–397 段(
text/22-fm-introduction.txt:391,搜「requests==2.19.1」)。 ↩ -
出处:「Introduction」(第 4 章)第 427–535 段(
text/22-fm-introduction.txt:445,搜「Trivy」;text/22-fm-introduction.txt:535,搜「Table 4.2」)。 ↩ -
出处:「Introduction」(第 4 章)第 541–577 段(
text/22-fm-introduction.txt:544,搜「trivy fs」;text/22-fm-introduction.txt:568,搜「requests │ HIGH」)。 ↩ -
出处:「Introduction」(第 4 章)第 583–586 段(
text/22-fm-introduction.txt:586,搜「mandatory stage」)。 ↩ -
出处:「Introduction」(第 4 章)第 592–595 段(
text/22-fm-introduction.txt:592,搜「Dependabot」)。 ↩ -
出处:「Introduction」(第 4 章)第 607 段(
text/22-fm-introduction.txt:607,搜「without executing」)与第 619 段(text/22-fm-introduction.txt:619,搜「black-box testing」)。 ↩ -
出处:「Introduction」(第 4 章)第 646–754 段(
text/22-fm-introduction.txt:754,搜「Table 4.3」)。 ↩ -
出处:「Introduction」(第 4 章)第 766–778 段(
text/22-fm-introduction.txt:769,搜「ping」)。 ↩ -
出处:「Introduction」(第 4 章)第 784–808 段(
text/22-fm-introduction.txt:787,搜「Validate allowed input」)。 ↩ -
出处:「Introduction」(第 4 章)第 835–850 段(
text/22-fm-introduction.txt:847,搜「OR '1'='1」)。 ↩ -
出处:「Introduction」(第 4 章)第 913 段(
text/22-fm-introduction.txt:913,搜「staging or pre-production」)。 ↩ -
出处:「Introduction」(第 4 章)第 940–956 段(
text/22-fm-introduction.txt:956,搜「defense-in-depth」)。 ↩ -
出处:「Introduction」(第 4 章)第 1159–1213 段(
text/22-fm-introduction.txt:1183,搜「Total: 8」;text/22-fm-introduction.txt:1213,搜「unencrypted S3 buckets」)与第 1317–1351 段(text/22-fm-introduction.txt:1326,搜「aws_s3_bucket」)。 ↩ -
出处:「Introduction」(第 4 章)第 1359–1365 段(
text/22-fm-introduction.txt:1359,搜「70%」;text/22-fm-introduction.txt:1365,搜「80% reduction」)。 ↩ -
出处:「Introduction」(第 4 章)第 1368 段(
text/22-fm-introduction.txt:1368,搜「false positives」)。 ↩ -
出处:「Introduction」(第 4 章)第 1377 段(
text/22-fm-introduction.txt:1377,搜「human oversights」)。 ↩ -
出处:「Introduction」(第 4 章)第 1409–1418 段(
text/22-fm-introduction.txt:1409,搜「IDE plugins」;text/22-fm-introduction.txt:1415,搜「security champion」;text/22-fm-introduction.txt:1418,搜「Capture-the-Flag」)。 ↩ -
出处:「Introduction」(第 4 章)第 1427–1436 段(
text/22-fm-introduction.txt:1427,搜「abcd1234-secret」)。 ↩ -
出处:「Introduction」(第 4 章)第 1494–1596 段(
text/22-fm-introduction.txt:1529,搜「14 days」)。原书明确说明这些数字「表示跨团队观察到的趋势,而非通用基准」。 ↩ -
出处:「Introduction」(第 4 章)第 1611–1617 段(
text/22-fm-introduction.txt:1617,搜「50% reduction」)。 ↩