跳转至

安全的代码,安全的系统 — 左移安全与供应链防线

这一章讲什么: 一个漏洞在开发阶段修要几分钟,上线后再修可能贵 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。

落到日常三个机制:

  1. 检查贴着动作做:IDE 插件在代码提交前就划波浪线;Git 钩子(pre-commit 检查) 硬拦硬编码的密钥;反馈越靠近出错点,越容易被当成帮助而不是 obstruction;
  2. 安全冠军(security champion):每个小队提名一名开发者,接受额外的安全编码与威胁建模训练, 当队里的第一联络人——把抽象风险翻译成队友能执行的动作;
  3. 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. 边界与局限

  1. 威胁建模被一笔带过——只在左移清单里出现一次,没有任何展开;这是全书安全的最大缺口之一。
  2. 案例数字全部无出处(-70%、80%、50%、30%……),引用时注意是示意量级。
  3. 密钥管理只给了两行:环境变量和 Secrets Manager/Vault,轮换、审计、最小权限这些 生产实践没展开。
  4. 没有讲扫描工具自身的成本与延迟——流水线里每加一道门都是有账的,本书默认门越多越好, 没有讨论门的预算。
  5. AI 生成代码的安全风险在第 01 章点了名,本章完全没有接住(代码评审责任、AI 引入的依赖风险)。

9. 可带走的

  1. 漏洞越晚发现越贵:生产修复 ≈ 设计阶段修复成本的 100 倍(原书口径),而且补丁出来前就在被打;
  2. 左移 = 安全检查嵌进每个阶段,不是加一道末端闸门;
  3. SQL 注入的根因是「数据和命令拼在一起」;参数化查询从机制上根除它;
  4. 依赖是最大暗面:传递依赖 + 拼写仿冒;Log4Shell 的教训是「你不知道自己用了什么」;
  5. Trivy 一条命令扫依赖/镜像/IaC,Fixed In 列直接给修复版本;
  6. SAST 看代码不看行为(早、误报多),DAST 看行为不看代码(晚、抓运行时);组合使用;
  7. 高危漏洞当质量门:扫描不通过,构建就停;
  8. 工具之上是人:告警被当误报忽略是安全计划的默认死法;安全冠军机制有量化收益(重复漏洞 -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」)

  1. 出处:「Introduction」(第 4 章)第 70 段(text/22-fm-introduction.txt:70,搜「left-to-right」)。 ↩

  2. 出处:「Introduction」(第 4 章)第 61 段(text/22-fm-introduction.txt:61,搜「100 times」)。 ↩

  3. 出处:「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」)。 ↩

  4. 出处:「Introduction」(第 4 章)第 122–230 段(text/22-fm-introduction.txt:230,搜「Table 4.1」)。 ↩

  5. 出处:「Introduction」(第 4 章)第 107 段(text/22-fm-introduction.txt:107,搜「enabler of agility」)。 ↩

  6. 出处:「Introduction」(第 4 章)第 242–290 段(text/22-fm-introduction.txt:254,搜「Vulnerable to SQL Injection」;text/22-fm-introduction.txt:287,搜「cursor.execute(query, (username,))」)。 ↩

  7. 出处:「Introduction」(第 4 章)第 296–311 段(text/22-fm-introduction.txt:299,搜「Code phase」)。 ↩

  8. 出处:「Introduction」(第 4 章)第 327–343 段(text/22-fm-introduction.txt:331,搜「OWASP Top 10」;text/22-fm-introduction.txt:343,搜「pull request comments」)。 ↩

  9. 出处:「Introduction」(第 4 章)第 350–353 段(text/22-fm-introduction.txt:353,搜「transitive dependencies」)。 ↩

  10. 出处:「Introduction」(第 4 章)第 359 段(text/22-fm-introduction.txt:359,搜「Log4Shell」)。 ↩

  11. 出处:「Introduction」(第 4 章)第 362 段(text/22-fm-introduction.txt:362,搜「typosquatting」)。 ↩

  12. 出处:「Introduction」(第 4 章)第 365–378 段(text/22-fm-introduction.txt:369,搜「Unpatched vulnerabilities」;text/22-fm-introduction.txt:378,搜「License risks」)。 ↩

  13. 出处:「Introduction」(第 4 章)第 385–397 段(text/22-fm-introduction.txt:391,搜「requests==2.19.1」)。 ↩

  14. 出处:「Introduction」(第 4 章)第 427–535 段(text/22-fm-introduction.txt:445,搜「Trivy」;text/22-fm-introduction.txt:535,搜「Table 4.2」)。 ↩

  15. 出处:「Introduction」(第 4 章)第 541–577 段(text/22-fm-introduction.txt:544,搜「trivy fs」;text/22-fm-introduction.txt:568,搜「requests │ HIGH」)。 ↩

  16. 出处:「Introduction」(第 4 章)第 583–586 段(text/22-fm-introduction.txt:586,搜「mandatory stage」)。 ↩

  17. 出处:「Introduction」(第 4 章)第 592–595 段(text/22-fm-introduction.txt:592,搜「Dependabot」)。 ↩

  18. 出处:「Introduction」(第 4 章)第 607 段(text/22-fm-introduction.txt:607,搜「without executing」)与第 619 段(text/22-fm-introduction.txt:619,搜「black-box testing」)。 ↩

  19. 出处:「Introduction」(第 4 章)第 646–754 段(text/22-fm-introduction.txt:754,搜「Table 4.3」)。 ↩

  20. 出处:「Introduction」(第 4 章)第 766–778 段(text/22-fm-introduction.txt:769,搜「ping」)。 ↩

  21. 出处:「Introduction」(第 4 章)第 784–808 段(text/22-fm-introduction.txt:787,搜「Validate allowed input」)。 ↩

  22. 出处:「Introduction」(第 4 章)第 835–850 段(text/22-fm-introduction.txt:847,搜「OR '1'='1」)。 ↩

  23. 出处:「Introduction」(第 4 章)第 913 段(text/22-fm-introduction.txt:913,搜「staging or pre-production」)。 ↩

  24. 出处:「Introduction」(第 4 章)第 940–956 段(text/22-fm-introduction.txt:956,搜「defense-in-depth」)。 ↩

  25. 出处:「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」)。 ↩

  26. 出处:「Introduction」(第 4 章)第 1359–1365 段(text/22-fm-introduction.txt:1359,搜「70%」;text/22-fm-introduction.txt:1365,搜「80% reduction」)。 ↩

  27. 出处:「Introduction」(第 4 章)第 1368 段(text/22-fm-introduction.txt:1368,搜「false positives」)。 ↩

  28. 出处:「Introduction」(第 4 章)第 1377 段(text/22-fm-introduction.txt:1377,搜「human oversights」)。 ↩

  29. 出处:「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」)。 ↩

  30. 出处:「Introduction」(第 4 章)第 1427–1436 段(text/22-fm-introduction.txt:1427,搜「abcd1234-secret」)。 ↩

  31. 出处:「Introduction」(第 4 章)第 1494–1596 段(text/22-fm-introduction.txt:1529,搜「14 days」)。原书明确说明这些数字「表示跨团队观察到的趋势,而非通用基准」。 ↩

  32. 出处:「Introduction」(第 4 章)第 1611–1617 段(text/22-fm-introduction.txt:1617,搜「50% reduction」)。 ↩