| name | performing-api-fuzzing-with-restler |
| description | 使用 Microsoft RESTler 执行有状态 REST API 模糊测试(Fuzzing),通过自动生成并执行测试序列来 覆盖 API 端点,发现请求间的生产者-消费者依赖关系,并找出安全性和可靠性缺陷。 测试人员将 OpenAPI 规范编译为 RESTler 模糊测试语法,配置认证,运行 test/fuzz-lean/fuzz 模式,并分析结果以发现 500 错误、认证绕过、资源泄漏和载荷注入漏洞。 当请求涉及 API 模糊测试、RESTler 测试、有状态 API 测试或自动化 API 安全扫描时触发。
|
| domain | cybersecurity |
| subdomain | api-security |
| tags | ["api-security","fuzzing","restler","automated-testing","openapi","stateful-testing"] |
| version | 1.0.0 |
| author | mahipal |
| license | Apache-2.0 |
使用 RESTler 执行 API 模糊测试
适用场景
- 使用 OpenAPI/Swagger 规范对 REST API 执行自动化安全测试
- 发现只有在特定 API 调用序列下才会出现的缺陷(有状态测试)
- 查找表明存在未处理异常或崩溃条件的 500 内部服务器错误响应
- 通过使用畸形、边界值和注入载荷对参数进行模糊测试来测试 API 输入验证
- 在 CI/CD 流水线中针对 API 变更运行持续的安全回归测试
不适用于 在未经明确授权和监控的情况下针对生产环境使用。RESTler 在模糊测试期间会大量创建和删除资源。
前置条件
- 明确授权文件,指定目标 API 和可接受的测试范围
- 已安装 Python 3.12+ 和 .NET 8.0 运行时
- 已从 https://github.com/microsoft/restler-fuzzer 下载 RESTler
- 目标 API 的 OpenAPI/Swagger 规范(v2 或 v3)
- API 认证凭据(令牌、API 密钥或 OAuth 凭据)
- 隔离的测试/预发布环境(RESTler 每小时可创建数千个资源)
工作流程
步骤 1:RESTler 安装与配置
git clone https://github.com/microsoft/restler-fuzzer.git
cd restler-fuzzer
python3 ./build-restler.py --dest_dir /opt/restler
/opt/restler/restler/Restler --help
步骤 2:编译 API 规范
/opt/restler/restler/Restler compile \
--api_spec /path/to/openapi.yaml
用于定向模糊测试的自定义字典(dict.json):
{
"restler_fuzzable_string": [
"fuzzstring",
"' OR '1'='1",
"\" OR \"1\"=\"1",
"<script>alert(1)</script>",
"../../../etc/passwd",
"${7*7}",
"{{7*7}}",
"a]UNION SELECT 1,2,3--",
"\"; cat /etc/passwd; echo \"",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
],
"restler_fuzzable_int": [
"0",
"-1",
"999999999",
"2147483647",
"-2147483648"
],
"restler_fuzzable_bool": ["true", "false", "null", "1", "0"],
"restler_fuzzable_datetime"
步骤 3:配置认证
import requests
import json
import time
class AuthenticationProvider:
def __init__(self):
self.token = None
self.token_expiry = 0
self.auth_url = "https://target-api.example.com/api/v1/auth/login"
self.credentials = {
"email": "fuzzer@test.com",
"password": "FuzzerPass123!"
}
def get_token(self):
"""获取或刷新认证令牌。"""
current_time = time.time()
if self.token and current_time < self.token_expiry - 60:
return self.token
resp = requests.post(self.auth_url, json=self.credentials)
if resp.status_code == 200:
data = resp.json()
self.token = data["access_token"]
self.token_expiry = current_time + 3600
return self.token
else:
raise Exception(f"认证失败:")
():
token = .get_token()
auth = AuthenticationProvider()
(auth.get_auth_header())
用于认证的引擎设置(engine_settings.json):
{
"authentication": {
"token": {
"token_refresh_interval": 300,
"token_refresh_cmd": "python3 /path/to/authentication_token.py"
}
},
"max_combinations": 20,
"max_request_execution_time": 30,
"global_producer_timing_delay": 2,
"no_ssl": false,
"host": "target-api.example.com",
"target_port": 443,
"garbage_collection_interval": 300,
"max_sequence_length": 10
}
步骤 4:以测试模式运行 RESTler(冒烟测试)
/opt/restler/restler/Restler test \
--grammar_file Compile/grammar.py \
--dictionary_file Compile/dict.json \
--settings Compile/engine_settings.json \
--no_ssl \
--target_ip target-api.example.com \
--target_port 443
cat Test/ResponseBuckets/runSummary.json
import json
with open("Test/ResponseBuckets/runSummary.json") as f:
summary = json.load(f)
print("测试模式摘要:")
print(f" 总请求数:{summary.get('total_requests_sent', {}).get('num_requests', 0)}")
print(f" 成功(2xx):{summary.get('num_fully_valid', 0)}")
print(f" 客户端错误(4xx):{summary.get('num_invalid', 0)}")
print(f" 服务器错误(5xx):{summary.get('num_server_error', 0)}")
covered = summary.get('covered_endpoints', [])
total = summary.get('total_endpoints', [])
uncovered = set(total) - set(covered)
if uncovered:
print(f"\n未覆盖的端点({len(uncovered)} 个):")
for ep in uncovered:
print(f" - {ep}")
步骤 5:运行 Fuzz-Lean 模式
/opt/restler/restler/Restler fuzz-lean \
--grammar_file Compile/grammar.py \
--dictionary_file Compile/dict.json \
--settings Compile/engine_settings.json \
--target_ip target-api.example.com \
--target_port 443 \
--time_budget 1
步骤 6:运行完整模糊测试模式
/opt/restler/restler/Restler fuzz \
--grammar_file Compile/grammar.py \
--dictionary_file Compile/dict.json \
--settings Compile/engine_settings.json \
--target_ip target-api.example.com \
--target_port 443 \
--time_budget 4 \
--enable_checkers UseAfterFree NamespaceRule ResourceHierarchy LeakageRule InvalidDynamicObject PayloadBody
python3 <<'EOF'
import json
import os
results_dir = "Fuzz/ResponseBuckets"
bugs_dir = "Fuzz/bug_buckets"
if os.path.exists(bugs_dir):
for bug_file in os.listdir(bugs_dir):
if bug_file.endswith(".txt"):
with open(os.path.join(bugs_dir, bug_file)) as f:
content = f.read()
print(f"\n=== 缺陷:{bug_file} ===")
print(content[:500])
summary_file = os.path.join(results_dir, "runSummary.json")
if os.path.exists(summary_file):
with open(summary_file) as f:
summary = json.load(f)
print(f"\n模糊测试摘要:")
print(f" 持续时间:{summary.get('time_budget_hours', 0)} 小时")
print(f" 总请求数:{summary.get('total_requests_sent', {}).get('num_requests', 0)}")
print(f" 发现缺陷数:{summary.get('num_bugs', 0)}")
print(f" 500 错误数:{summary.get('num_server_error', 0)}")
EOF
核心概念
| 术语 | 定义 |
|---|
| 有状态模糊测试(Stateful Fuzzing) | 通过使用早期请求的响应作为后续请求的输入来维持跨请求状态的 API 模糊测试,可测试多步骤工作流 |
| 生产者-消费者依赖(Producer-Consumer Dependencies) | RESTler 的推断机制:一个 API 调用产生的值(如创建的资源 ID)应被后续调用所消费 |
| 模糊测试语法(Fuzzing Grammar) | API 规范的编译表示,定义如何为每个端点生成有效和无效的请求 |
| 检查器(Checker) | 测试特定漏洞模式(如释放后使用、命名空间隔离或信息泄露)的 RESTler 安全规则 |
| 缺陷桶(Bug Bucket) | RESTler 按类型和端点对发现的缺陷进行分类,将类似故障分组以便高效分诊 |
| 垃圾回收(Garbage Collection) | RESTler 定期清理在模糊测试期间创建的资源,防止目标系统资源耗尽 |
工具与系统
- RESTler:Microsoft Research 的有状态 REST API 模糊测试工具,将 OpenAPI 规范编译为模糊测试语法
- Schemathesis:基于属性的 API 测试工具,从 OpenAPI/GraphQL 模式生成测试用例
- Dredd:根据 OpenAPI/API Blueprint 文档验证 API 实现的测试工具
- Fuzz-lightyear:Yelp 的无状态 API 模糊器,专注于发现认证和授权漏洞
- API Fuzzer:OWASP 的 API 端点模糊测试工具,支持自定义载荷字典
常见场景
场景:微服务 API 模糊测试活动
场景背景:一家金融科技公司拥有 12 个配有 OpenAPI 规范的微服务 API。在重大发布前,安全团队在预发布环境中对每个服务运行 RESTler 模糊测试以发现缺陷。
方法:
- 收集所有 12 个服务的 OpenAPI 规范,将每个规范编译为 RESTler 语法
- 使用服务专属凭据为每个服务配置认证
- 对每个服务运行测试模式,验证端点可达性并修复语法问题
- 对每个服务运行 fuzz-lean 模式(每服务 1 小时)以快速发现问题
- 在 fuzz-lean 模式中发现 23 个缺陷:8 个未处理的 500 错误、5 个释放后使用模式、4 个命名空间隔离失败、6 个错误响应中的信息泄露
- 对缺陷最多的 5 个服务运行完整模糊测试模式(每服务 4 小时)
- 发现 47 个额外缺陷,包括一个严重的认证绕过:删除用户后重用其令牌仍可访问
- 生成缺陷报告并通过 JIRA 集成跟踪修复进度
常见陷阱:
- 未了解 RESTler 会创建和删除数千个资源,就对生产环境运行
- 未正确配置认证,导致 RESTler 只测试未认证访问
- 使用默认字典,未添加应用特定的注入载荷
- 未设置时间预算,导致 RESTler 无限运行
- 忽略编译警告,这些警告指示 RESTler 因依赖问题无法到达的端点
输出格式
## RESTler API 模糊测试报告
**目标**:用户服务 API(staging.example.com)
**规范**:OpenAPI 3.0(42 个端点)
**持续时间**:4 小时(完整模糊测试模式)
**总请求数**:145,832
### 缺陷摘要
| 类别 | 数量 | 严重性 |
|----------|-------|----------|
| 500 内部服务器错误 | 12 | 高 |
| 释放后使用 | 3 | 严重 |
| 命名空间规则违规 | 5 | 严重 |
| 信息泄露 | 8 | 中 |
| 资源泄漏 | 4 | 低 |
### 关键发现
**1. 释放后使用:已删除用户的令牌仍然有效**
- 序列:POST /users -> DELETE /users/{id} -> GET /users/{id}
- 删除用户后,使用已删除用户的令牌发送 GET 请求返回 200
- 影响:已删除账户仍可访问 API
**2. 命名空间违规:跨租户数据访问**
- 序列:POST /users(租户 A)-> GET /users/{id}(租户 B 令牌)
- 租户 A 创建的用户可通过租户 B 的凭据访问
- 影响:多租户隔离被突破
**3. 500 错误:未处理的整数溢出**
- 请求:POST /orders {"quantity": 2147483648}
- 响应:500 内部服务器错误,包含堆栈跟踪
- 影响:DoS 潜力,通过堆栈跟踪造成信息泄露
### 覆盖率
- 端点覆盖:38/42(90.5%)
- 未覆盖:POST /admin/migrate、DELETE /admin/cache、
PUT /config/advanced、POST /webhooks/test