SAST(静态应用安全测试)
某大型电商公司在发布前进行了一次安全大检查。安全团队使用了 SAST 工具扫描了全部 200 万行代码,结果令人震惊:工具在 30 分钟内发现了 3,847 个潜在的安全问题。
人工复核后发现,其中 1,293 个是真实漏洞,84 个是高危漏洞。更关键的是,这些漏洞分布在 23 个不同的微服务中,如果在上线后被发现,后果不堪设想。
这次扫描让公司意识到:光靠代码审查和渗透测试,远远不够。你需要一种能够自动化、规模化发现代码安全问题的技术。
这就是 SAST。
SAST 的定义
SAST(Static Application Security Testing,静态应用安全测试)是一种在不运行代码的情况下,通过分析源代码的语法、结构和控制流等信息来发现安全漏洞的技术。
flowchart LR
A["源代码"] --> B["词法分析器"]
B --> C["语法分析器"]
C --> D["AST<br/>(抽象语法树)"]
D --> E["语义分析器"]
E --> F["安全规则引擎"]
F --> G["漏洞报告"]
style F fill:#54a0ff
style G fill:#ff6b6b
工作原理
数据流分析
追踪数据从输入源到敏感操作的流向,检测是否存在不安全的传播:
DataFlowExample.java
public void processRequest(HttpServletRequest request) {
// Source:用户输入(污点源)
String userInput = request.getParameter("username");
// 中间传播
String query = "SELECT * FROM users WHERE name = '" + userInput + "'";
// Sink:敏感操作(污点槽)
statement.execute(query); // SQL 注入!
}
SAST 工具会追踪 userInput 的数据流,检测它是否被直接拼接到 SQL 语句中。
控制流分析
分析代码的执行路径,识别条件分支和循环中的安全问题:
ControlFlowExample.java
public void withdraw(HttpServletRequest request) {
boolean isVIP = checkVIP(request.getParameter("userId"));
if (isVIP) {
// VIP 可以提取更多,但这个检查可能被绕过
amount = Long.MAX_VALUE; // 未校验
} else {
amount = Long.parseLong(request.getParameter("amount"));
if (amount > balance) {
throw new InsufficientFundsException();
}
}
// 存在绕过普通用户限制的代码路径
account.withdraw(amount);
}
抽象解释
通过模拟程序执行状态,在不实际运行代码的情况下验证安全属性:
AbstractInterpretation.java
public class PasswordChecker {
// 工具会分析:password 是否可能被设为 null 或空字符串
public boolean isValid(String password) {
if (password == null || password.isEmpty()) {
return false;
}
// 如果到这里,password 一定非空
return password.length() >= 8;
}
public void setUserPassword(String password) {
// 工具会分析:password 是否经过 isValid 检查
// 如果没有,报告漏洞
this.password = password;
}
}
工具对比
企业级工具
开源工具
Semgrep 规则示例
Semgrep 是一个轻量级但功能强大的静态分析工具,支持用 YAML 编写规则:
sql-injection-rule.yaml
rules:
# SQL 注入检测规则
- id: java-sql-injection
pattern: |
$STMT.executeQuery($QUERY)
message: |
SQL 注入漏洞:用户输入直接拼接到 SQL 查询中。
攻击者可以通过构造恶意输入来执行任意 SQL 语句。
severity: ERROR
languages:
- java
metadata:
cwe: CWE-89
owasp: A03:2021
paths:
include:
- "*.java"
patterns:
- pattern-not: |
PreparedStatement $PS = ...;
$PS.setString(...);
$PS.executeQuery(...);
- pattern: |
$QUERY = "...SELECT... " + $INPUT + "...";
$STMT.executeQuery($QUERY);
VulnerableCode.java
// Semgrep 会检测到 SQL 注入
String query = "SELECT * FROM users WHERE name = '" + username + "'";
stmt.executeQuery(query);
path-traversal-rule.yaml
rules:
# 路径遍历检测规则
- id: path-traversal
pattern: |
new File($PATH + $INPUT)
message: |
潜在路径遍历漏洞:用户输入被拼接到文件路径中。
攻击者可能使用 "../" 来访问敏感文件。
severity: ERROR
languages:
- java
metadata:
cwe: CWE-22
CodeQL 查询示例
SqlInjection.ql
/**
* @name SQL Injection
* @description Constructing SQL queries by concatenating user input can lead to SQL injection.
* @kind path-problem
* @problem.severity error
* @precision high
*/
import java
import semmle.code.java.security.SqlInjection
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink
where sqlInjection(source, sink)
select sink.getNode(), source, sink,
"SQL injection from $@.", source.getNode()
优势与局限性
SAST 的优势
flowchart LR
A["无需运行"] --> E["早期发现"]
B["全代码覆盖"] --> E
C["自动化集成"] --> E
D["可发现多种漏洞"] --> E
E --> F["安全左移<br/>降低修复成本"]
style A fill:#54a0ff
style B fill:#54a0ff
style C fill:#54a0ff
style D fill:#54a0ff
style F fill:#48dbfb
SAST 的局限性
:::warning 重要认知
SAST 不是银弹,了解其局限性才能正确使用:
- 误报率高:SAST 工具会产生大量误报,需要人工复核
- 无法发现运行时漏洞:配置错误、环境相关问题无法检测
- 无法检测业务逻辑漏洞:越权、认证绕过等业务逻辑问题难以发现
- 语言和框架限制:对某些语言或框架的支持不够完善
- 规则覆盖度:依赖规则库,新型漏洞可能检测不到
- 性能问题:大型项目的扫描可能耗时较长
:::
SDLC 集成
IDE 集成(Shift Left)
在开发者编写代码时即时发现问题:
.vscode/extensions.json
{
"recommendations": [
"semgrep.semgrep",
"sonarsource.sonarlint-vs-code"
]
}
sonarlint.properties
# SonarLint 配置
sonar.java.source=17
# 规则质量配置
sonar.java.file.symlinks.enabled=true
# 忽略的规则(需评估后确认)
sonar.issue.ignore.all=squid:S1234
Git Hooks 集成
.githooks/pre-commit
#!/bin/sh
# pre-commit hook - 增量代码扫描
echo "Running pre-commit security scan..."
# 只扫描暂存的 Java 文件
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.java$')
if [ -n "$STAGED_FILES" ]; then
semgrep --config=p/java --quiet $STAGED_FILES
if [ $? -ne 0 ]; then
echo "Security issues found in staged files. Please fix before committing."
exit 1
fi
fi
echo "Pre-commit security scan passed."
CI/CD 集成
GitHub
name: SAST Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >
p/java-secrets
p/java-best practices
p/owasp-top-ten
# CI 模式下只报告新问题
baselineCommit: ${{ github.event.pull_request.base.sha }}
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: semgrep.sarif
GitLab
sast:
stage: security
image: returntocorp/semgrep
script:
- semgrep --config=p/java --json --output=semgrep-report.json .
artifacts:
reports:
sast: semgrep-report.json
when: always
规则调优
降低误报率
semgrepignore
# 忽略特定目录
src/test/
src/docs/
# 忽略特定文件
**/*Test.java
**/*Mock*.java
# 忽略特定规则
rules:
- id: java-hardcoded-credential
severity: WARNING # 降级为警告
mode: false
定制业务规则
custom-rules/business-logic.yaml
rules:
# 检测内部 API 直接暴露
- id: internal-api-exposed
pattern: |
@GetMapping("/api/internal/...")
message: |
内部 API 不应直接暴露到公网。请使用 @Security annotation 或配置网络访问控制。
severity: WARNING
languages:
- java
metadata:
category: business-logic
# 检测敏感数据日志记录
- id: sensitive-data-logged
pattern: |
logger.$METHOD("$LOGMSG", $ARG)
message: |
日志中可能包含敏感信息。请使用 DataMasker 进行脱敏。
severity: ERROR
languages:
- java
metadata:
category: data-protection
filters:
- metavariable-regex:
variable: LOGMSG
regex: "(password|token|secret|key|card|cvv)"
SAST 与代码审查
SAST 不能替代代码审查,但可以增强代码审查:
flowchart LR
A["开发者提交代码"] --> B["SAST 自动扫描"]
B --> C{"发现问题?"}
C -->|是| D["人工复核"]
C -->|否| E["进入代码审查"]
D --> F{"确认漏洞?"}
F -->|是| G["返回修复"]
F -->|否| H["标记为误报"]
H --> E
G --> I["重新提交"]
I --> B
E --> J["合并到主干"]
style D fill:#feca57
style J fill:#26de81
思考题
问题 1:SAST 工具报告了 500 个安全问题,其中 80% 被人工复核认定为误报。作为安全团队负责人,你会如何降低误报率,同时保持对真实漏洞的检测能力?
参考答案
降低误报率的策略:
- 规则精细化
# 原来的宽泛规则
- pattern: $OBJ.$METHOD($ARG)
# 精细化后的规则
- pattern: |
@Autowired
$SVC $SVC_NAME;
...
$SVC_NAME.$METHOD($ARG)
# 添加更多前置条件,减少误报
- 使用数据流分析减少误报
- 追踪污点从 source 到 sink 的完整路径
- 如果中间经过安全处理(如转义、校验),则不报告
- 配置路径过滤
- 分层报告
# 高置信度规则 - 强制执行
p/command-injection
p/sql-injection
# 中置信度规则 - 警告
p/xxe
p/path-traversal
# 低置信度规则 - 仅记录
p/crypto-weak
- 持续优化
- 每次误报后,分析原因,更新规则或添加排除
- 建立误报白名单,记录经过人工确认的安全代码
- 与代码审查结合
- 将 SAST 报告作为代码审查的辅助输入
- 审查者评估每个发现是否真实漏洞
平衡策略:保持高召回率(不漏报),通过精细化和人工复核控制误报率。
问题 2:某公司决定将 SAST 集成到 CI/CD 流程中,但开发团队抱怨 SAST 扫描时间太长(每次提交需要 45 分钟),影响开发效率。作为 DevSecOps 工程师,你会如何优化?
参考答案
优化方案:
- 增量扫描
# 只扫描变更的文件
git diff --cached --name-only --diff-filter=ACM > changed_files.txt
semgrep --targets changed_files.txt
# 定时执行全量扫描(如每日一次)
cron: "0 2 * * *" # 凌晨 2 点
- 并行化扫描
jobs:
semgrep:
runs-on: ubuntu-latest
strategy:
matrix:
chunk: [1, 2, 3, 4]
script:
# 将文件列表分成 4 份,并行扫描
semgrep --targets chunk-${{ matrix.chunk }}.txt
- 缓存机制
- name: Cache Semgrep rules
uses: actions/cache@v3
with:
path: ~/.cache/semgrep
key: semgrep-rules-${{ hashFiles('.semgrep/**') }}
- 分层策略
# PR 阶段:只扫描变更 + 关键规则(快速)
- semgrep --config=p/owasp-top-ten --fast
# 合主干时:完整扫描(中速)
- semgrep --config=p/all
# 定时任务:完整深度扫描(慢速,可接受)
- semgrep --config=p/all --deep
- 结果复用
# 对比当前扫描与上次扫描,只报告新增问题
semgrep --baseline-commit=<previous-commit>
- 异步报告
# PR 时快速返回,深度报告异步发送
script:
- semgrep --config=p/owasp-top-ten --json --output=quick.json --fast
# 异步任务
- semgrep --config=p/all --json --output=deep.json &
- 架构优化
- 使用 SaaS 版本,利用云端算力
- 将扫描结果存储在数据库中,支持增量对比