performing-api-fuzzing-with-restler

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Performing API Fuzzing with RESTler

使用RESTler进行API模糊测试

When to Use

适用场景

  • Performing automated security testing of REST APIs using their OpenAPI/Swagger specifications
  • Discovering bugs that only manifest through specific sequences of API calls (stateful testing)
  • Finding 500 Internal Server Error responses that indicate unhandled exceptions or crash conditions
  • Testing API input validation by fuzzing parameters with malformed, boundary, and injection payloads
  • Running continuous security regression testing in CI/CD pipelines for API changes
Do not use against production environments without explicit authorization and monitoring. RESTler creates and deletes resources aggressively during fuzzing.
  • 借助OpenAPI/Swagger规范对REST API执行自动化安全测试
  • 发现仅在特定API调用序列下才会显现的漏洞(有状态测试)
  • 排查指示未处理异常或崩溃情况的500内部服务器错误响应
  • 通过向参数输入畸形、边界值和注入Payload来测试API的输入验证能力
  • 在CI/CD流水线中针对API变更运行持续安全回归测试
注意:未经明确授权和监控,请勿在生产环境中使用。RESTler在模糊测试过程中会大量创建和删除资源。

Prerequisites

前置条件

  • Written authorization specifying the target API and acceptable testing scope
  • Python 3.12+ and .NET 8.0 runtime installed
  • RESTler downloaded from https://github.com/microsoft/restler-fuzzer
  • OpenAPI/Swagger specification (v2 or v3) for the target API
  • API authentication credentials (tokens, API keys, or OAuth credentials)
  • Isolated test/staging environment (RESTler can create thousands of resources per hour)
  • 针对目标API及可测试范围的书面授权
  • 已安装Python 3.12+和.NET 8.0运行时
  • 已从https://github.com/microsoft/restler-fuzzer下载RESTler
  • 目标API的OpenAPI/Swagger规范(v2或v3版本)
  • API认证凭据(令牌、API密钥或OAuth凭据)
  • 隔离的测试/预发布环境(RESTler每小时可创建数千个资源)

Workflow

工作流程

Step 1: RESTler Installation and Setup

步骤1:RESTler安装与设置

bash
undefined
bash
undefined

Clone and build RESTler

克隆并构建RESTler

Build RESTler

构建RESTler

python3 ./build-restler.py --dest_dir /opt/restler
python3 ./build-restler.py --dest_dir /opt/restler

Verify installation

验证安装

/opt/restler/restler/Restler --help
/opt/restler/restler/Restler --help

Alternative: Use pre-built release

替代方案:使用预构建版本

undefined
undefined

Step 2: Compile the API Specification

步骤2:编译API规范

bash
undefined
bash
undefined

Compile the OpenAPI spec into a RESTler fuzzing grammar

将OpenAPI规范编译为RESTler模糊测试语法

/opt/restler/restler/Restler compile
--api_spec /path/to/openapi.yaml
/opt/restler/restler/Restler compile
--api_spec /path/to/openapi.yaml

Output directory structure:

输出目录结构:

Compile/

Compile/

grammar.py - Generated fuzzing grammar

grammar.py - 生成的模糊测试语法

grammar.json - Grammar in JSON format

grammar.json - JSON格式的语法文件

dict.json - Custom dictionary for fuzzing values

dict.json - 用于模糊测试的自定义字典

engine_settings.json - Engine configuration

engine_settings.json - 引擎配置文件

config.json - Compilation config

config.json - 编译配置文件


**Custom dictionary for targeted fuzzing (dict.json):**
```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": [
        "2024-01-01T00:00:00Z",
        "0000-00-00T00:00:00Z",
        "9999-12-31T23:59:59Z",
        "invalid-date"
    ],
    "restler_fuzzable_uuid4": [
        "00000000-0000-0000-0000-000000000000",
        "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
    ],
    "restler_custom_payload": {
        "/users/{userId}": ["1", "0", "-1", "admin", "' OR 1=1--"],
        "/orders/{orderId}": ["1", "0", "999999999"]
    }
}

**用于定向模糊测试的自定义字典(dict.json):**
```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": [
        "2024-01-01T00:00:00Z",
        "0000-00-00T00:00:00Z",
        "9999-12-31T23:59:59Z",
        "invalid-date"
    ],
    "restler_fuzzable_uuid4": [
        "00000000-0000-0000-0000-000000000000",
        "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
    ],
    "restler_custom_payload": {
        "/users/{userId}": ["1", "0", "-1", "admin", "' OR 1=1--"],
        "/orders/{orderId}": ["1", "0", "999999999"]
    }
}

Step 3: Configure Authentication

步骤3:配置认证

python
undefined
python
undefined

authentication_token.py - RESTler authentication module

authentication_token.py - RESTler认证模块

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):
    """Get or refresh authentication token."""
    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  # Assume 1-hour TTL
        return self.token
    else:
        raise Exception(f"Authentication failed: {resp.status_code}")

def get_auth_header(self):
    """Return the authentication header for RESTler."""
    token = self.get_token()
    return f"Authorization: Bearer {token}"
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  # 假设令牌有效期为1小时
        return self.token
    else:
        raise Exception(f"认证失败: {resp.status_code}")

def get_auth_header(self):
    """返回供RESTler使用的认证头。"""
    token = self.get_token()
    return f"Authorization: Bearer {token}"

Export the token refresh command for RESTler

导出供RESTler使用的令牌刷新命令

auth = AuthenticationProvider() print(auth.get_auth_header())

**Engine settings for authentication (engine_settings.json):**
```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
}
auth = AuthenticationProvider() print(auth.get_auth_header())

**用于认证的引擎设置(engine_settings.json):**
```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
}

Step 4: Run RESTler in Test Mode (Smoke Test)

步骤4:以测试模式运行RESTler(冒烟测试)

bash
undefined
bash
undefined

Test mode: Quick validation that all endpoints are reachable

测试模式:快速验证所有端点是否可达

/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
/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

Review test results

查看测试结果

cat Test/ResponseBuckets/runSummary.json

```python
cat Test/ResponseBuckets/runSummary.json

```python

Parse test results

解析测试结果

import json
with open("Test/ResponseBuckets/runSummary.json") as f: summary = json.load(f)
print("Test Mode Summary:") print(f" Total requests: {summary.get('total_requests_sent', {}).get('num_requests', 0)}") print(f" Successful (2xx): {summary.get('num_fully_valid', 0)}") print(f" Client errors (4xx): {summary.get('num_invalid', 0)}") print(f" Server errors (5xx): {summary.get('num_server_error', 0)}")
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)}")

Identify uncovered endpoints

识别未覆盖的端点

covered = summary.get('covered_endpoints', []) total = summary.get('total_endpoints', []) uncovered = set(total) - set(covered) if uncovered: print(f"\nUncovered endpoints ({len(uncovered)}):") for ep in uncovered: print(f" - {ep}")
undefined
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}")
undefined

Step 5: Run Fuzz-Lean Mode

步骤5:运行Fuzz-Lean模式

bash
undefined
bash
undefined

Fuzz-lean: One pass through all endpoints with security checkers enabled

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 # 1 hour max
/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 # 最长1小时

Checkers automatically enabled in fuzz-lean:

Fuzz-lean模式下自动启用的检查器:

- UseAfterFree: Tests accessing resources after deletion

- UseAfterFree: 测试资源删除后的访问情况

- NamespaceRule: Tests accessing resources across namespaces/tenants

- NamespaceRule: 测试跨命名空间/租户的资源访问

- ResourceHierarchy: Tests child resources with wrong parent IDs

- ResourceHierarchy: 测试使用错误父ID的子资源

- LeakageRule: Tests for information disclosure in error responses

- LeakageRule: 测试错误响应中的信息泄露

- InvalidDynamicObject: Tests with malformed dynamic object IDs

- InvalidDynamicObject: 测试使用畸形动态对象ID的情况

undefined
undefined

Step 6: Run Full Fuzzing Mode

步骤6:运行完整模糊测试模式

bash
undefined
bash
undefined

Full fuzz mode: Extended fuzzing for comprehensive coverage

完整模糊测试模式:扩展模糊测试以实现全面覆盖

/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
/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

Analyze fuzzing results

分析模糊测试结果

python3 <<'EOF' import json import os
results_dir = "Fuzz/ResponseBuckets" bugs_dir = "Fuzz/bug_buckets"
python3 <<'EOF' import json import os
results_dir = "Fuzz/ResponseBuckets" bugs_dir = "Fuzz/bug_buckets"

Parse 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: {bug_file} ===") print(content[:500])
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])

Parse response summary

解析响应总结

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"\nFuzz Summary:") print(f" Duration: {summary.get('time_budget_hours', 0)} hours") print(f" Total requests: {summary.get('total_requests_sent', {}).get('num_requests', 0)}") print(f" Bugs found: {summary.get('num_bugs', 0)}") print(f" 500 errors: {summary.get('num_server_error', 0)}") EOF
undefined
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
undefined

Key Concepts

核心概念

TermDefinition
Stateful FuzzingAPI fuzzing that maintains state across requests by using responses from earlier requests as inputs to later ones, enabling testing of multi-step workflows
Producer-Consumer DependenciesRESTler's inference that a value produced by one API call (e.g., a created resource ID) should be consumed by a subsequent call
Fuzzing GrammarCompiled representation of the API specification that defines how to generate valid and invalid requests for each endpoint
CheckerRESTler security rule that tests for specific vulnerability patterns like use-after-free, namespace isolation, or information leakage
Bug BucketRESTler's categorization of discovered bugs by type and endpoint, grouping similar failures for efficient triage
Garbage CollectionRESTler's periodic cleanup of resources created during fuzzing to prevent resource exhaustion on the target system
术语定义
有状态模糊测试跨请求维护状态的API模糊测试,将早期请求的响应作为后续请求的输入,支持多步骤工作流测试
生产者-消费者依赖关系RESTler推断出的依赖关系:某个API调用生成的值(如创建的资源ID)应被后续调用使用
模糊测试语法API规范的编译表示,定义如何为每个端点生成有效和无效的请求
检查器RESTler的安全规则,用于测试特定漏洞模式,如释放后使用、命名空间隔离或信息泄露
漏洞分类RESTler按类型和端点对发现的漏洞进行分类,将类似故障分组以提高排查效率
垃圾回收RESTler定期清理模糊测试过程中创建的资源,以防止目标系统资源耗尽

Tools & Systems

工具与系统

  • RESTler: Microsoft Research's stateful REST API fuzzing tool that compiles OpenAPI specs into fuzzing grammars
  • Schemathesis: Property-based API testing tool that generates test cases from OpenAPI/GraphQL schemas
  • Dredd: API testing tool that validates API implementations against OpenAPI/API Blueprint documentation
  • Fuzz-lightyear: Yelp's stateless API fuzzer focused on finding authentication and authorization vulnerabilities
  • API Fuzzer: OWASP tool for API endpoint fuzzing with customizable payload dictionaries
  • RESTler: 微软研究院开发的有状态REST API模糊测试工具,可将OpenAPI规范编译为模糊测试语法
  • Schemathesis: 基于属性的API测试工具,从OpenAPI/GraphQL schema生成测试用例
  • Dredd: API测试工具,验证API实现是否符合OpenAPI/API Blueprint文档
  • Fuzz-lightyear: Yelp开发的无状态API模糊测试工具,专注于发现认证与授权漏洞
  • API Fuzzer: OWASP推出的API端点模糊测试工具,支持自定义Payload字典

Common Scenarios

常见场景

Scenario: Microservice API Fuzzing Campaign

场景:微服务API模糊测试活动

Context: A fintech company has 12 microservice APIs with OpenAPI specifications. Before a major release, the security team runs RESTler fuzzing against each service in the staging environment to catch bugs.
Approach:
  1. Collect OpenAPI specs for all 12 services and compile each into a RESTler grammar
  2. Configure authentication for each service with service-specific credentials
  3. Run test mode on each service to validate endpoint reachability and fix grammar issues
  4. Run fuzz-lean mode (1 hour per service) to identify quick wins
  5. Find 23 bugs in fuzz-lean mode: 8 unhandled 500 errors, 5 use-after-free patterns, 4 namespace isolation failures, 6 information leakage in error responses
  6. Run full fuzz mode (4 hours per service) on the 5 services with the most bugs
  7. Discover 47 additional bugs including a critical authentication bypass where deleting a user and reusing their token still allows access
  8. Generate bug reports and track remediation through JIRA integration
Pitfalls:
  • Running RESTler against production without understanding that it creates and deletes thousands of resources
  • Not configuring authentication correctly, causing RESTler to only test unauthenticated access
  • Using the default dictionary without adding application-specific injection payloads
  • Not setting a time budget, allowing RESTler to run indefinitely
  • Ignoring the compilation warnings that indicate endpoints RESTler cannot reach due to dependency issues
背景: 某金融科技公司拥有12个带OpenAPI规范的微服务API。在重大版本发布前,安全团队在预发布环境中对每个服务运行RESTler模糊测试以排查漏洞。
实施步骤:
  1. 收集所有12个服务的OpenAPI规范,并将每个规范编译为RESTler语法
  2. 为每个服务配置专属的认证凭据
  3. 在每个服务上运行测试模式,验证端点可达性并修复语法问题
  4. 运行fuzz-lean模式(每个服务1小时)以快速发现明显漏洞
  5. 在fuzz-lean模式下发现23个漏洞:8个未处理500错误、5个释放后使用模式、4个命名空间隔离失败、6个错误响应中的信息泄露
  6. 对漏洞最多的5个服务运行完整模糊测试模式(每个服务4小时)
  7. 额外发现47个漏洞,包括一个严重的认证绕过漏洞:删除用户后复用其令牌仍可访问系统
  8. 生成漏洞报告并通过JIRA集成跟踪修复进度
常见误区:
  • 在未了解RESTler会创建和删除数千个资源的情况下,直接在生产环境中运行
  • 未正确配置认证,导致RESTler仅测试未认证的访问路径
  • 使用默认字典而未添加针对应用的注入Payload
  • 未设置时间预算,导致RESTler无限期运行
  • 忽略编译警告,这些警告通常表示RESTler因依赖问题无法访问某些端点

Output Format

输出格式

undefined
undefined

RESTler API Fuzzing Report

RESTler API模糊测试报告

Target: User Service API (staging.example.com) Specification: OpenAPI 3.0 (42 endpoints) Duration: 4 hours (full fuzz mode) Total Requests: 145,832
目标: 用户服务API (staging.example.com) 规范: OpenAPI 3.0 (42个端点) 持续时间: 4小时(完整模糊测试模式) 总请求数: 145,832

Bug Summary

漏洞总结

CategoryCountSeverity
500 Internal Server Error12High
Use After Free3Critical
Namespace Rule Violation5Critical
Information Leakage8Medium
Resource Leak4Low
分类数量严重程度
500内部服务器错误12
释放后使用3严重
命名空间规则违反5严重
信息泄露8
资源泄漏4

Critical Findings

严重发现

1. Use-After-Free: Deleted user token still valid
  • Sequence: POST /users -> DELETE /users/{id} -> GET /users/{id}
  • After deleting user, GET with the deleted user's token returns 200
  • Impact: Deleted accounts can still access the API
2. Namespace Violation: Cross-tenant data access
  • Sequence: POST /users (tenant A) -> GET /users/{id} (tenant B token)
  • User created by tenant A is accessible with tenant B's credentials
  • Impact: Multi-tenant isolation breach
3. 500 Error: Unhandled integer overflow
  • Request: POST /orders {"quantity": 2147483648}
  • Response: 500 Internal Server Error with stack trace
  • Impact: DoS potential, information disclosure via stack trace
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内部服务器错误并返回堆栈跟踪
  • 影响: 存在拒绝服务风险,堆栈跟踪可能导致信息泄露

Coverage

覆盖范围

  • Endpoints covered: 38/42 (90.5%)
  • Uncovered: POST /admin/migrate, DELETE /admin/cache, PUT /config/advanced, POST /webhooks/test
undefined
  • 已覆盖端点: 38/42 (90.5%)
  • 未覆盖端点: POST /admin/migrate, DELETE /admin/cache, PUT /config/advanced, POST /webhooks/test
undefined