API 安全测试#
某金融公司在上线新版 API 前,进行了例行的功能测试和性能测试。系统上线一周后,安全团队在例行审计中发现,有大量异常请求尝试访问管理员接口。原来攻击者通过 API 文档发现了几个被遗忘的旧版本接口,这些接口没有任何权限校验。
事后复盘发现,如果在上线前进行过一次完整的安全测试,这个漏洞本可以避免。功能测试验证的是「功能是否正常工作」,而安全测试验证的是「功能在异常情况下是否还能正常工作」。两者同等重要,但很多团队只做了前者。
#一、API 安全测试方法论
#测试金字塔
API 安全测试应该遵循测试金字塔原则:
flowchart TB
subgraph Top["安全测试金字塔"]
PT[渗透测试<br/>Penetration Testing<br/>少量、高价值]
SAST[安全代码审计<br/>Code Review<br/>中量]
DAST[动态扫描<br/>DAST/Fuzzing<br/>大量、自动化]
end
style PT fill:#ffcccc
style SAST fill:#ffffcc
style DAST fill:#ccffcc| 层级 | 方法 | 覆盖率 | 成本 | 时机 |
|---|---|---|---|---|
| 基础层 | 自动化扫描、Fuzzing | 高 | 低 | 每次构建 |
| 中间层 | 安全代码审查、SAST | 中 | 中 | 每次提交 |
| 顶层 | 渗透测试 | 低 | 高 | 上线前 |
#测试覆盖维度
完整的 API 安全测试应该覆盖以下维度:
| 维度 | 测试内容 | 典型工具 |
|---|---|---|
| 认证安全 | Token 强度、会话管理、密码策略 | Burp Suite、OWASP ZAP |
| 授权安全 | 越权访问、权限绕过 | Postman、自定义脚本 |
| 输入验证 | 注入攻击、XSS、格式校验 | Fuzzer、SQL Map |
| 业务逻辑 | 绕过验证、负数金额、时间旅行 | 手工测试、API 模拟 |
| 速率限制 | 暴力破解、资源耗尽 | JMeter、Locust |
| 敏感数据 | 数据泄露、加密传输 | Burp Suite、mitmproxy |
#二、手工测试
手工测试虽然效率低,但能发现自动化工具难以检测的业务逻辑漏洞。
#认证绕过测试
# 测试场景:尝试绕过身份验证
# 1. 空 Token
GET /api/v1/users/me
Authorization: Bearer
# 2. 伪造 Token(使用已知密钥签名)
GET /api/v1/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 3. 删除 Token 头部
GET /api/v1/users/me
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# 4. 使用已撤销的 Token
GET /api/v1/users/me
Authorization: Bearer <revoked_token>
# 5. 测试认证失败后的响应
POST /api/v1/auth/login
Content-Type: application/json
{"username":"admin","password":"wrongpassword"}#越权访问测试(IDOR)
# 测试场景:失效的对象级授权
# 1. 尝试访问他人资源
GET /api/v1/orders/12345
Authorization: Bearer <user_a_token>
# 2. 修改资源 ID,尝试越权
GET /api/v1/orders/12346
Authorization: Bearer <user_a_token>
# 3. 尝试负数 ID
GET /api/v1/orders/-1
Authorization: Bearer <user_a_token>
# 4. 尝试数组 ID(批量访问)
GET /api/v1/orders/[1,2,3,4,5]
Authorization: Bearer <user_a_token>
# 5. 尝试模糊 ID
GET /api/v1/orders/12*
Authorization: Bearer <user_a_token>
# 6. 尝试特殊字符
GET /api/v1/orders/' OR '1'='1
Authorization: Bearer <user_a_token>
# 7. 修改响应包提升权限
GET /api/v1/users/me
# 响应: {"role": "user"}
# 修改为
PUT /api/v1/users/me
{"role": "admin"}#SQL 注入测试
# 测试场景:SQL 注入攻击
# 1. 基础注入测试
GET /api/v1/users?search=' OR '1'='1
GET /api/v1/users?search=admin'--
# 2. UNION 注入
GET /api/v1/users?search=' UNION SELECT null,username,password,null FROM users--
# 3. 布尔盲注
GET /api/v1/users?search=admin' AND 1=1--
GET /api/v1/users?search=admin' AND 1=2--
# 4. 时间盲注
GET /api/v1/users?search=admin' AND SLEEP(5)--
# 5. JSON 参数注入
POST /api/v1/search
Content-Type: application/json
{"query": "admin'--"}
{"query": "admin\"; DROP TABLE users;--"}#参数污染测试
# 测试场景:HTTP 参数污染
# 同一参数多次提交
GET /api/v1/users?id=1&id=2&id=3
GET /api/v1/users?id=1,2,3
GET /api/v1/users?id[]=1&id[]=2
# JSON 和表单参数混合
POST /api/v1/users
Content-Type: application/x-www-form-urlencoded
username=admin&data={"role":"admin"}
# 大小写混淆
GET /api/v1/ADMIN/users
GET /api/v1/users/ADMIN#三、自动化测试工具
#Postman + Newman
Postman 是最流行的 API 测试工具,结合 Newman 可以实现自动化:
Postman
{
"info": {
"name": "API Security Tests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "baseUrl",
"value": "https://api.example.com"
}
],
"item": [
{
"name": "认证绕过测试",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('应返回401认证失败', function() {",
" pm.response.to.have.status(401);",
"});",
"",
"pm.test('不应泄露用户信息', function() {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.not.have.property('username');",
" pm.expect(jsonData).to.not.have.property('email');",
"});"
]
}
}
],
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/users/me",
"header": []
}
},
{
"name": "越权访问测试",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('不应访问其他用户的订单', function() {",
" var jsonData = pm.response.json();",
" // 检查返回的订单不属于当前用户",
" if (jsonData.orderId !== pm.collectionVariables.get('userOrderId')) {",
" pm.expect.fail('检测到越权访问漏洞');",
" }",
"});"
]
}
}
],
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/orders/{{otherUserOrderId}}",
"header": [
{
"key": "Authorization",
"value": "Bearer {{userToken}}"
}
]
}
}
]
}Newman
# 安装 Newman
npm install -g newman
# 运行安全测试集合
newman run security-tests.postman_collection.json \
--environment prod-env.json \
--reporters cli,junit \
--reporter-junit-export output/results.xml
# 失败时返回非零退出码
if [ $? -ne 0 ]; then
echo "安全测试失败,阻止部署"
exit 1
fi#OWASP ZAP
OWASP Zed Attack Proxy 是免费的开源 Web 安全扫描器:
ZAP
# 使用 Docker 运行 ZAP
docker run -t owasp/zap2docker-stable zap-api-scan.py \
-t https://api.example.com/openapi.json \
-f openapi \
-r report.html
# 基础认证和 Bearer Token
docker run -t owasp/zap2docker-stable zap-api-scan.py \
-t https://api.example.com/swagger.yaml \
-f openapi \
-z "-config api.addheader.Authorization='Bearer {{token}}'" \
-r report.html
# 使用 CI 模式
docker run -t owasp/zap2docker-stable zap-full-scan.py \
-t https://api.example.com \
-J zap-report.json \
--context /zap/wrk/context.context#Burp Suite
Burp Suite 是专业渗透测试工具:
Burp
# 使用 Burp Scanner 进行 API 扫描
java -jar burpsuite_pro.jar \
--project-file=api-test.b旭 \
--dependency-check=false \
--target-file=targets.txt
# 与 CI 集成
# 导出 Burp 报告
docker run --rm -v $(pwd):/output \
portswigger/burp-api-scan \
--api-url https://api.example.com/swagger.json \
--output /output/burp-report.html#四、Fuzzing 测试
Fuzzing 通过生成大量畸形输入来发现潜在漏洞:
#Fuzzing 策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| 随机 Fuzz | 随机生成输入 | 快速发现崩溃 |
| 字典 Fuzz | 基于已知关键词生成 | 注入类漏洞 |
| 协议 Fuzz | 基于协议规范生成 | 协议解析漏洞 |
| 生成式 Fuzz | 基于语法生成有效输入 | 结构化数据漏洞 |
| 进化式 Fuzz | 根据反馈优化输入 | 高覆盖率 |
#API Fuzzing 实现
API
public class ApiFuzzer {
private final RestTemplate restTemplate;
private final PayloadGenerator generator;
private final VulnerabilityReporter reporter;
/**
* Fuzzing 测试入口
*/
public FuzzingReport fuzz(String baseUrl, ApiEndpoint endpoint) {
FuzzingReport report = new FuzzingReport(endpoint);
// 1. 获取 API 规范
OpenApiSpec spec = loadApiSpec(baseUrl);
// 2. 对每个端点进行 Fuzzing
for (Operation operation : spec.getOperations(endpoint)) {
List<FuzzingResult> results = fuzzOperation(
baseUrl, operation
);
report.addResults(operation, results);
}
return report;
}
/**
* 对单个操作进行 Fuzzing
*/
private List<FuzzingResult> fuzzOperation(String baseUrl, Operation operation) {
List<FuzzingResult> results = new ArrayList<>();
// 1. Fuzz 每个参数
for (Parameter param : operation.getParameters()) {
results.addAll(fuzzParameter(baseUrl, operation, param));
}
// 2. Fuzz 请求体
if (operation.getRequestBody() != null) {
results.addAll(fuzzRequestBody(baseUrl, operation));
}
// 3. Fuzz HTTP 头
results.addAll(fuzzHeaders(baseUrl, operation));
return results;
}
/**
* Fuzz 字符串参数
*/
private List<FuzzingResult> fuzzParameter(String baseUrl,
Operation operation,
Parameter param) {
List<FuzzingResult> results = new ArrayList<>();
// SQL 注入 payloads
List<String> sqlPayloads = List.of(
"' OR '1'='1",
"' OR '1'='1' --",
"'; DROP TABLE users;--",
"1' AND '1'='1",
"admin'--"
);
// XSS payloads
List<String> xssPayloads = List.of(
"<script>alert(1)</script>",
"javascript:alert(1)",
"<img src=x onerror=alert(1)>",
"{{constructor.constructor('alert(1)')()}}"
);
// 命令注入 payloads
List<String> cmdPayloads = List.of(
"; ls",
"| cat /etc/passwd",
"`whoami`",
"$(whoami)"
);
// 长字符串测试
List<String> overflowPayloads = List.of(
"A".repeat(1000),
"A".repeat(10000),
"A".repeat(100000)
);
for (String payload : sqlPayloads) {
FuzzingResult result = sendFuzzedRequest(
baseUrl, operation, param, payload, VulnerabilityType.SQL_INJECTION
);
if (result.isVulnerable()) {
results.add(result);
}
}
// ... 对其他 payload 类型的处理类似
return results;
}
/**
* 发送 Fuzzed 请求
*/
private FuzzingResult sendFuzzedRequest(String baseUrl,
Operation operation,
Parameter param,
String payload,
VulnerabilityType type) {
try {
Map<String, Object> params = new HashMap<>();
params.put(param.getName(), payload);
ResponseEntity<String> response = restTemplate.exchange(
buildUrl(baseUrl, operation, params),
HttpMethod.valueOf(operation.getMethod().toUpperCase()),
buildHttpEntity(operation, params),
String.class
);
// 分析响应判断是否漏洞
return analyzeResponse(operation, param, payload, response, type);
} catch (Exception e) {
// 异常可能表示发现了漏洞
return FuzzingResult.vulnerable(
operation, param, payload, type, e.getMessage()
);
}
}
private FuzzingResult analyzeResponse(Operation operation,
Parameter param,
String payload,
ResponseEntity<String> response,
VulnerabilityType type) {
String body = response.getBody();
int statusCode = response.getStatusCode().value();
switch (type) {
case SQL_INJECTION:
// 检查 SQL 错误信息泄露
if (body != null && (
body.contains("SQL syntax") ||
body.contains("mysql_fetch") ||
body.contains("ORA-") ||
body.contains("SQLServer")
)) {
return FuzzingResult.vulnerable(
operation, param, payload, type,
"SQL 错误信息泄露"
);
}
break;
case XSS:
// 检查输入是否被反射
if (body != null && body.contains(payload)) {
// 检查是否有编码或转义
if (!isProperlyEscaped(body, payload)) {
return FuzzingResult.vulnerable(
operation, param, payload, type,
"输入未经过滤直接反射"
);
}
}
break;
}
return FuzzingResult.notVulnerable(operation, param, payload, type);
}
}#使用 FFUF 进行 API Fuzzing
FFUF
# 安装
go install github.com/ffuf/ffuf@latest
# 目录和参数 Fuzzing
ffuf -w wordlists/api-paths.txt \
-u https://api.example.com/api/v1/FUZZ \
-H "Authorization: Bearer $TOKEN" \
-mc 200,401,403 \
-o results.json
# 参数值 Fuzzing
ffuf -w wordlists/sql-injection.txt \
-u "https://api.example.com/api/v1/users?id=FUZZ" \
-H "Authorization: Bearer $TOKEN" \
-mr "SQL syntax" \
-o sqli-results.json
# JSON body Fuzzing
ffuf -w wordlists/fuzzing.txt \
-u https://api.example.com/api/v1/search \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"FUZZ"}' \
-mr "error\|exception\|traceback" \
-o fuzz-results.json#五、渗透测试流程
#渗透测试阶段
flowchart LR
subgraph Recon["情报收集"]
R1[API 发现]
R2[端点枚举]
R3[指纹识别]
end
subgraph Analyze["分析与建模"]
A1[请求分析]
A2[参数识别]
A3[认证机制分析]
end
subgraph Exploit["漏洞利用"]
E1[注入测试]
E2[越权测试]
E3[认证绕过]
end
subgraph Report["报告"]
RP1[漏洞记录]
RP2[风险评估]
RP3[修复建议]
end
Recon --> Analyze --> Exploit --> Report#情报收集
API
# 1. 发现 API 文档
# robots.txt
curl https://api.example.com/robots.txt
# Swagger/OpenAPI
curl https://api.example.com/swagger.json
curl https://api.example.com/api-docs
# 2. 枚举 API 版本
curl https://api.example.com/api/v1/health
curl https://api.example.com/api/v2/health
curl https://api.example.com/api/v3/health
# 3. 指纹识别
curl -I https://api.example.com/api/v1/users
# 检查 Server、CORS、认证头
# 4. GraphQL 探测
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name}}}"}'#漏洞利用
越权漏洞利用脚本
public class IDORExploiter {
/**
* 测试批量 IDOR
*/
public List<IDORFinding> testBulkIDOR(String baseUrl, String token,
int startId, int endId) {
List<IDORFinding> findings = new ArrayList<>();
RestTemplate client = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
for (int id = startId; id <= endId; id++) {
try {
ResponseEntity<String> response = client.exchange(
baseUrl + "/orders/" + id,
HttpMethod.GET,
new HttpEntity<>(headers),
String.class
);
if (response.getStatusCode().value() == 200) {
// 成功访问到他人资源
findings.add(new IDORFinding(
id,
response.getBody(),
determineSeverity(response)
));
}
} catch (Exception e) {
// 忽略错误
}
}
return findings;
}
/**
* 测试参数篡改
*/
public IDORFinding testParameterTampering(String baseUrl, String token) {
RestTemplate client = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
// 原始请求:更新自己的邮箱
Map<String, Object> originalBody = Map.of(
"email", "attacker@example.com"
);
// 尝试更新其他用户的邮箱
Map<String, Object> tamperedBody = Map.of(
"email", "attacker@example.com",
"userId", 12345 // 篡改用户 ID
);
ResponseEntity<String> response = client.exchange(
baseUrl + "/users/me",
HttpMethod.PUT,
new HttpEntity<>(tamperedBody, headers),
String.class
);
if (response.getStatusCode().value() == 200) {
// 验证是否真的修改了他人邮箱
ResponseEntity<User> victimResponse = client.exchange(
baseUrl + "/users/12345",
HttpMethod.GET,
new HttpEntity<>(headers),
User.class
);
if ("attacker@example.com".equals(victimResponse.getBody().getEmail())) {
return new IDORFinding(12345, true);
}
}
return IDORFinding.notVulnerable();
}
}#六、CI/CD 集成
#安全测试集成到流水线
.gitlab-ci.yml
stages:
- test
- security
- deploy
variables:
ZAP_API_TARGET: "https://api.example.com/swagger.json"
# 单元测试
unit-tests:
stage: test
script:
- mvn test
coverage: '/TOTAL.* ([0-9]{1,3})%/'
# 安全测试
api-security-scan:
stage: security
image: owasp/zap2docker-stable:latest
script:
- zap-api-scan.py -t $ZAP_API_TARGET -f openapi -r zap_report.html
- zap-baseline.py -t https://api.example.com -J zap_baseline.json
artifacts:
reports:
security: zap_report.html
paths:
- zap_report.html
- zap_baseline.json
expire_in: 30 days
allow_failure: true # 允许扫描失败,不阻塞部署
# Fuzzing 测试
api-fuzzing:
stage: security
image: maven:3.8-openjdk-17
script:
- mvn exec:java -Dfuzz.skip=false
artifacts:
reports:
junit: target/fuzzing-results.xml
paths:
- target/fuzzing-results.json
only:
- schedules # 定期执行,不阻塞每次部署
# 渗透测试(仅在 scheduled pipelines)
penetration-test:
stage: security
image: Kali
script:
- export BURP_KEY=$BURP_API_KEY
- python3 burp_scanner.py --target api.example.com --output report.html
artifacts:
paths:
- report.html
only:
- schedules
when: manual#GitHub Actions 集成
.github/workflows/security-test.yml
name: API Security Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 2 * * *' # 每天凌晨执行
jobs:
zap-scan:
name: ZAP API Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ZAP Scan
uses: zaproxy/action-api-scan@v0.7.0
with:
target: 'https://api.example.com/openapi.json'
token: ${{ secrets.ZAP_API_KEY }}
- name: Upload Report
uses: actions/upload-artifact@v4
with:
name: zap-report
path: zap_report.html
- name: Fail on High Vulnerabilities
if: ${{ github.event_name == 'pull_request' }}
run: |
HIGH_COUNT=$(cat zap_report.json | jq '.site[0].alerts | map(select(.riskdesc | contains("High"))) | length')
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "发现 $HIGH_COUNT 个高危漏洞"
exit 1
fi
fuzzing:
name: API Fuzzing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Run Fuzzing
run: mvn exec:java -Dfuzz.skip=false
- name: Upload Fuzzing Results
uses: actions/upload-artifact@v4
with:
name: fuzzing-results
path: target/fuzzing-results.json#七、测试环境 vs 生产环境
| 维度 | 测试环境 | 生产环境 |
|---|---|---|
| 数据 | 脱敏的测试数据 | 真实用户数据 |
| 权限 | 可开启调试模式 | 安全模式 |
| 限流 | 宽松限制 | 严格限制 |
| 影响 | 可接受破坏性测试 | 禁止破坏性测试 |
| 监控 | 完整日志 | 受限日志 |
| 工具 | 所有工具可用 | 有限工具 |
警告
在生产环境进行任何安全测试前,必须获得明确的书面授权。未授权的安全测试在很多司法管辖区属于违法行为。
#八、安全测试报告
安全测试报告生成
public class SecurityTestReport {
private String projectName;
private String environment;
private List<Vulnerability> vulnerabilities;
private List<SecurityMetric> metrics;
private LocalDateTime scanTime;
public String generateMarkdown() {
StringBuilder report = new StringBuilder();
report.append("# API 安全测试报告\n\n");
report.append("**项目**: ").append(projectName).append("\n");
report.append("**环境**: ").append(environment).append("\n");
report.append("**扫描时间**: ").append(scanTime).append("\n\n");
// 执行摘要
report.append("## 执行摘要\n\n");
report.append(generateExecutiveSummary()).append("\n\n");
// 漏洞统计
report.append("## 漏洞统计\n\n");
report.append(generateVulnerabilityStats()).append("\n\n");
// 漏洞详情
report.append("## 漏洞详情\n\n");
for (Vulnerability vuln : vulnerabilities) {
report.append(generateVulnerabilitySection(vuln)).append("\n\n");
}
// 修复建议
report.append("## 修复建议\n\n");
report.append(generateRemediationPlan()).append("\n\n");
return report.toString();
}
private String generateVulnerabilitySection(Vulnerability vuln) {
return String.format("""
### %s (%s)
**严重程度**: %s
**发现时间**: %s
**影响**:
%s
**复现步骤**:
%s
**修复建议**:
%s
""",
vuln.getTitle(),
vuln.getCweId(),
vuln.getSeverity(),
vuln.getDiscoveredAt(),
vuln.getImpact(),
vuln.getReproductionSteps(),
vuln.getRemediation()
);
}
}#思考题
问题 1:在 API 安全测试中,如何区分「真正的漏洞」和「误报」?哪些测试结果应该引起重视,哪些可能是误报?
参考答案
判断真正漏洞 vs 误报的标准:
-
可复现性:
- 真正的漏洞:多次请求都能触发相同结果
- 误报:偶发性触发,通常是竞态条件或环境问题
-
上下文分析:
- 真正的漏洞:攻击者能从中获取非法利益
- 误报:看似异常但实际无害
-
危害评估:
- 真正的漏洞:有明确的安全影响
- 误报:没有实际危害或无法利用
-
具体判断示例:
- SQL 注入:需要验证数据库是否真的被影响,而非仅检测到 SQL 语法
- XSS:需要验证脚本是否真的被执行,而非仅检测到反射
- 越权访问:需要验证是否真的访问到他人的私密数据
-
验证步骤:
- 使用不同的 payload 重试
- 在测试环境完整复现
- 分析响应数据的实际含义
- 评估攻击的可行性和影响范围
问题 2:自动化安全测试工具(如 ZAP、Burp Suite)的扫描结果应该如何处理?哪些漏洞可以自动修复,哪些需要人工干预?
参考答案
扫描结果处理策略:
可自动修复的漏洞:
-
配置类漏洞:
- 缺少安全响应头(X-Frame-Options、CSP)
- CORS 配置过于宽松
- 调试模式未关闭
- 版本信息泄露
-
简单输入验证:
- 缺少参数长度限制
- 缺少字符类型校验
-
低风险告警:
- 信息泄露(如详细的错误信息)
- 自动添加到 ignore list
需要人工干预的漏洞:
-
业务逻辑漏洞:
- 越权访问(需要理解业务上下文)
- 业务流程绕过(需要完整流程分析)
- 认证机制缺陷(需要深入分析)
-
复杂注入:
- 嵌套注入
- 编码绕过
- 上下文敏感的注入
-
架构设计问题:
- 加密算法选择
- 密钥管理方案
- 整体安全架构
建议流程:
- 自动化工具扫描
- 自动修复低风险配置问题
- 安全工程师审查所有发现
- 对真正漏洞进行优先级排序
- 制定修复计划并跟踪
- 修复后重新测试验证