Fuzzing REST APIs with RESTler and Schemathesis
Most API fuzzing tools throw random bytes at endpoints and hope for 500 errors. RESTler and Schemathesis are different: they understand your API's structure from an OpenAPI or Swagger spec and generate semantically valid but edge-case inputs. This makes them far more effective at finding real bugs — authentication bypasses, state machine violations, resource exhaustion, and crashes that random input would never reach.
Why API Fuzzing Is Different
Binary fuzzing targets functions that read bytes. REST API fuzzing targets HTTP endpoints that read structured data. The challenges are different:
- Inputs must be structurally valid JSON (or XML) to get past basic validation
- Many endpoints require authentication tokens
- Endpoints have dependencies: you must
POST /usersbefore you canGET /users/{id} - State matters: deleting a resource changes what other requests return
Generic fuzzers handle none of this. RESTler and Schemathesis handle all of it.
Schemathesis: Property-Based API Testing
Schemathesis is a Python tool that uses property-based testing (via Hypothesis) to generate test cases from your OpenAPI spec. It's the easiest way to start fuzzing an API.
Installation
pip install schemathesis
# or via pipx for isolation
pipx install schemathesisBasic Usage
Point it at your API spec:
# Against a live server
st run https://api.example.com/openapi.json
# Against a local server
st run http://localhost:8000/openapi.json --base-url http://localhost:8000
# Against a spec file with a local server
st run ./openapi.yaml --base-url http://localhost:8000Schemathesis will generate hundreds of test cases covering:
- Valid inputs with boundary values
- Empty strings, null values, very long strings
- Special characters:
null,\x00, SQL injection strings, path traversal - Wrong types (sending a string where an integer is expected)
- Missing required fields
What It Looks For
By default, Schemathesis checks for:
- 5xx responses: Any server error is a bug
- Response schema conformance: Does the response match the spec?
- Content-Type correctness: Does the server return what it claims?
You can add custom checks:
# custom_checks.py
import schemathesis
@schemathesis.check
def no_redirect_to_external(response, case):
if response.is_redirect:
location = response.headers.get("Location", "")
assert "example.com" in location, f"Unexpected redirect to {location}"st run openapi.json --checks all --base-url http://localhost:8000Authenticated Endpoints
Most real APIs require authentication. Schemathesis handles this via headers:
# Bearer token
st run openapi.json \
--header "Authorization: Bearer eyJ..." \
--base-url http://localhost:8000
# API key
st run openapi.json \
--header "X-API-Key: your-key-here" \
--base-url http://localhost:8000For OAuth2 flows, use the Python API:
import schemathesis
import requests
schema = schemathesis.from_uri("http://localhost:8000/openapi.json")
def get_token():
r = requests.post("http://localhost:8000/auth/token", json={
"username": "test@example.com",
"password": "testpass"
})
return r.json()["access_token"]
@schema.parametrize()
def test_api(case):
case.headers = case.headers or {}
case.headers["Authorization"] = f"Bearer {get_token()}"
response = case.call()
case.validate_response(response)Running as pytest
Schemathesis integrates directly with pytest:
pip install schemathesis[pytest]
pytest --schemathesis openapi.json --base-url http://localhost:8000Or write explicit test files:
# test_api_fuzz.py
import schemathesis
schema = schemathesis.from_uri("http://localhost:8000/openapi.json")
@schema.parametrize()
def test_no_server_errors(case):
response = case.call_and_validate()
assert response.status_code < 500RESTler: Stateful REST API Fuzzing
Schemathesis generates independent requests. RESTler goes further: it learns relationships between endpoints and generates multi-step sequences. If GET /orders/{orderId} requires a valid orderId, RESTler will first call POST /orders to create one, then use the returned ID in subsequent requests.
RESTler was developed by Microsoft Research and is particularly good at finding bugs in stateful workflows.
Installation
RESTler requires .NET 6+:
# Install .NET
wget https://dot.net/v1/dotnet-install.sh
bash dotnet-install.sh --channel 6.0
# Clone and build RESTler
git clone https://github.com/microsoft/restler-fuzzer
cd restler-fuzzer
python ./build-restler.py --dest_dir ~/restlerCompile Your Spec
RESTler first compiles your OpenAPI spec into a fuzzing grammar:
python ~/restler/restler/Restler.py compile \
--api_spec openapi.jsonThis creates a Compile/ directory with grammar.py — a Python file describing all endpoints and their dependencies.
Run the Fuzzer
RESTler has several modes:
# Test mode: check for 5xx on basic valid inputs
python ~/restler/restler/Restler.py test \
--grammar_file Compile/grammar.py \
--dictionary_file Compile/dict.json \
--settings restler_settings.json
# Fuzz-lean: fast fuzzing with limited mutations
python ~/restler/restler/Restler.py fuzz-lean \
--grammar_file Compile/grammar.py \
--dictionary_file Compile/dict.json \
--time_budget 1
# Full fuzz: exhaustive, time-bounded
python ~/restler/restler/Restler.py fuzz \
--grammar_file Compile/grammar.py \
--dictionary_file Compile/dict.json \
--time_budget 4--time_budget is in hours. For CI, fuzz-lean with a 10-minute budget is practical.
RESTler Settings File
{
"target_ip": "localhost",
"target_port": 8000,
"token_refresh_cmd": "python get_token.py",
"token_refresh_interval": 300,
"checkers": {
"leakagerule": { "enabled": true },
"resourcehierarchy": { "enabled": true },
"namespacerule": { "enabled": true },
"useafterfree": { "enabled": true },
"payloadbody": { "enabled": true }
}
}The checkers are where RESTler shines:
- leakagerule: Detects data leakage between users
- namespacerule: Finds authorization bypasses (can user A access user B's resources?)
- useafterfree: Accesses deleted resources to check if they're still accessible
- resourcehierarchy: Tests parent-child resource access control
RESTler Bug Report
RESTler writes bugs to FuzzLean/BugBuckets/ or Fuzz/BugBuckets/. Each bug is a reproduction sequence:
BugDetails: 500_ServerError
Sequence:
POST /api/users (201)
POST /api/orders {"userId": "<userId>"} (201)
DELETE /api/users/<userId> (204)
GET /api/orders/<orderId> (500) ← BUGThis is far more useful than a single crashing request because it shows you exactly what state the server needs to be in.
Combining Both Tools
Use them at different layers:
| Layer | Tool | What It Finds |
|---|---|---|
| Input validation | Schemathesis | 500 errors, schema violations, injection bugs |
| Authorization | RESTler checkers | Namespace/IDOR bugs, use-after-delete |
| State machine | RESTler sequences | Multi-step workflow bugs |
| Business logic | HelpMeTest + Playwright | User-visible behavior, UI flows |
For end-to-end browser tests and user journey validation, HelpMeTest covers what API fuzzers can't: verifying that the actual UI behaves correctly, forms submit as expected, and users see what they should. Fuzzing finds crashes; integration tests verify correctness.
CI Integration
Add a quick Schemathesis check to your pipeline:
# .github/workflows/api-fuzz.yml
name: API Fuzz
on: [push, pull_request]
jobs:
fuzz:
runs-on: ubuntu-latest
services:
api:
image: your-api:latest
ports: ["8000:8000"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install schemathesis
- name: Wait for API
run: |
until curl -sf http://localhost:8000/health; do sleep 1; done
- name: Fuzz
run: |
st run http://localhost:8000/openapi.json \
--checks all \
--max-examples 100 \
--base-url http://localhost:8000 \
--report report.html
- uses: actions/upload-artifact@v4
if: always()
with:
name: fuzz-report
path: report.html--max-examples 100 keeps CI fast. For nightly runs, drop the limit entirely.
Common Findings
In practice, Schemathesis and RESTler commonly find:
- Unhandled null values crashing the server where the spec says the field is optional
- Integer overflow in pagination (
?limit=-1,?page=9999999999) - Path traversal in endpoints that read files based on user input
- IDOR vulnerabilities where RESTler's namespace checker accesses other users' resources
- Uncaught database errors exposed as 500s with stack traces
These are exactly the bugs that manual testing misses because no human thinks to try ?limit=-1.
Start Small
Pick one service with an OpenAPI spec, run st run against a staging instance, and read the output. You'll have findings within minutes. That's the value of spec-based fuzzing: the spec does the heavy lifting of telling the fuzzer what's valid.