Fuzzing REST APIs with RESTler and Schemathesis

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 /users before you can GET /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 schemathesis

Basic 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:8000

Schemathesis 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:8000

Authenticated 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:8000

For 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:8000

Or 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 < 500

RESTler: 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 ~/restler

Compile Your Spec

RESTler first compiles your OpenAPI spec into a fuzzing grammar:

python ~/restler/restler/Restler.py compile \
    --api_spec openapi.json

This 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)  ← BUG

This 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.

Read more

Start now free