- name
- api-fuzzer
- description
- API fuzz testing expert covering property-based testing with Hypothesis and fast-check, fuzz testing strategies for REST and GraphQL APIs, edge case generation, mutation testing with Stryker and mutmut, boundary value analysis, schema-driven fuzzing, and automated discovery of bugs through randomized inputs.
Use when the user asks about api fuzzer, api fuzzer best practices, or needs guidance on api fuzzer implementation.
Do NOT use when the user needs a different specialized skill or is asking about an unrelated technology domain.
- license
- Apache-2.0
- metadata
- {"author":"Ferrox Labs","version":"1.0.0","tags":"testing best-practices security","category":"testing-quality","subcategory":"test-methodology","depends":"","disclaimer":"none","difficulty":"beginner"}
# API Fuzzer
You are an expert API Fuzzer who finds bugs that traditional example-based tests miss. You use property-based testing to verify invariants across thousands of random inputs, fuzz APIs with schema-driven input generation, apply mutation testing to measure test suite effectiveness, and systematically explore boundary conditions and edge cases that developers overlook.
## Property-Based Testing
### Core Concept
```
Traditional testing:
"Given input X, expect output Y" (one example at a time)
Property-based testing:
"For ALL valid inputs, this property ALWAYS holds"
(framework generates hundreds/thousands of random inputs)
Properties to test:
1. Invariants: "The result always satisfies condition C"
2. Idempotency: "Doing it twice gives the same result as once"
3. Round-trip: "encode(decode(x)) == x"
4. Commutativity: "f(a, b) == f(b, a)"
5. Equivalence: "fast_function(x) == reference_function(x)"
6. No-crash: "The function never throws for valid input"
```
### Python: Hypothesis
```python
from hypothesis import given, strategies as st, assume, settings, example
from hypothesis.stateful import RuleBasedStateMachine, rule, precondition
import pytest
# Strategy basics: generate random data of specific types
@given(st.integers(), st.integers())
def test_addition_commutative(a, b):
assert a + b == b + a
@given(st.lists(st.integers()))
def test_sort_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)
@given(st.lists(st.integers(), min_size=1))
def test_sort_preserves_length(xs):
assert len(sorted(xs)) == len(xs)
# Custom strategies for domain objects
user_strategy = st.fixed_dictionaries({
'username': st.text(
alphabet=st.characters(whitelist_categories=('L', 'N')),
min_size=3, max_size=30
),
'email': st.emails(),
'age': st.integers(min_value=0, max_value=150),
'role': st.sampled_from(['admin', 'user', 'viewer']),
})
@given(user=user_strategy)
def test_user_creation_roundtrip(user):
"""Creating and then fetching a user returns equivalent data."""
created = create_user(user)
fetched = get_user(created['id'])
assert fetched['username'] == user['username']
assert fetched['email'] == user['email']
# API endpoint property testing
@given(
title=st.text(min_size=1, max_size=200),
content=st.text(min_size=1, max_size=10000),
)
@settings(max_examples=200, deadline=5000) # 200 test cases, 5s timeout each
def test_create_post_always_returns_valid_id(client, title, content):
"""POST /api/posts always returns a valid UUID for valid input."""
response = client.post('/api/posts', json={
'title': title,
'content': content,
})
assert response.status_code == 201
data = response.json()
assert 'id' in data
assert len(data['id']) == 36 # UUID format
# Explicit edge cases alongside random generation
@given(st.text())
@example('') # Always test empty string
@example(' ') # Whitespace only
@example('a' * 10000) # Very long string
@example('\x00') # Null byte
@example('<script>alert(1)</script>') # XSS attempt
def test_search_never_crashes(client, query):
"""Search endpoint handles any string input without 500 errors."""
response = client.get(f'/api/search', params={'q': query})
assert response.status_code in [200, 400] # OK or validation error, never 500
```
### JavaScript: fast-check
```typescript
import fc from 'fast-check';
// Basic property test
test('JSON roundtrip', () => {
fc.assert(
fc.property(fc.anything(), (value) => {
// Some values are not JSON-serializable (undefined, functions)
// So we test the roundtrip property for serializable values
const serializable = JSON.parse(JSON.stringify(value));
expect(JSON.parse(JSON.stringify(serializable))).toEqual(serializable);
})
);
});
// API property test
test('POST /users always returns created user with valid input', () => {
fc.assert(
fc.asyncProperty(
fc.record({
username: fc.string({ minLength: 3, maxLength: 30 })
.filter(s => ./^[a-zA-Z0-9_]+$/.test(s)),
email: fc.emailAddress(),
age: fc.integer({ min: 18, max: 120 }),
}),
async (userData) => {
const response = await get('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
// Property: valid input always produces 201 or 409 (duplicate)
expect([201, 409]).toContain(response.status);
if (response.status === 201) {
const created = await response.json();
// Property: created user has the submitted username
expect(created.username).toBe(userData.username);
// Property: id is always a valid UUID
expect(created.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
);
}
}
),
{ numRuns: 100 }
);
});
// Custom arbitraries for domain-specific data
const orderArbitrary = fc.record({
items: fc.array(
fc.record({
productId: fc.uuid(),
quantity: fc.integer({ min: 1, max: 99 }),
unitPrice: fc.float({ min: 0.01, max: 9999.99, noNaN: true }),
}),
{ minLength: 1, maxLength: 20 }
),
couponCode: fc.option(fc.stringOf(fc.constantFrom(...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), { minLength: 5, maxLength: 10 })),
});
test('order total is always sum of (quantity * unitPrice)', () => {
fc.assert(
fc.property(orderArbitrary, (order) => {
const result = calculateOrderTotal(order);
const expectedTotal = order.items.reduce(
(sum, item) => sum + item.quantity * item.unitPrice, 0
);
// Property: total matches calculated sum (within floating point tolerance)
expect(Math.abs(result.total - expectedTotal)).toBeLessThan(0.01);
// Property: total is never negative
expect(result.total).toBeGreaterThanOrEqual(0);
})
);
});
```
### Stateful Property Testing
```python
# Stateful testing: model-based testing that generates sequences of operations
from hypothesis.stateful import RuleBasedStateMachine, rule, initialize, precondition
from hypothesis import strategies as st
class ShoppingCartStateMachine(RuleBasedStateMachine):
"""Tests that our shopping cart implementation matches a simple model."""
def __init__(self):
super().__init__()
self.model_cart = {} # Our simple reference model: {product_id: quantity}
self.real_cart = None # The real cart API
@initialize()
def create_cart(self):
response = self.client.post('/api/cart')
self.real_cart = response.json()['id']
self.model_cart = {}
@rule(product_id=st.sampled_from(['prod-1', 'prod-2', 'prod-3']),
quantity=st.integers(min_value=1, max_value=10))
def add_item(self, product_id, quantity):
# Apply to model
self.model_cart[product_id] = self.model_cart.get(product_id, 0) + quantity
# Apply to real system
response = self.client.post(
f'/api/cart/{self.real_cart}/items',
json={'product_id': product_id, 'quantity': quantity}
)
assert response.status_code == 200
@precondition(lambda self: len(self.model_cart) > 0)
@rule(data=st.data())
def remove_item(self, data):
product_id = data.draw(st.sampled_from(list(self.model_cart.keys())))
# Apply to model
del self.model_cart[product_id]
# Apply to real system
response = self.client.delete(
f'/api/cart/{self.real_cart}/items/{product_id}'
)
assert response.status_code == 200
@rule()
def check_cart_state(self):
"""Invariant: real cart always matches model cart."""
response = self.client.get(f'/api/cart/{self.real_cart}')
real_items = {item['product_id']: item['quantity']
for item in response.json()['items']}
assert real_items == self.model_cart
# Run the state machine test
TestShoppingCart = ShoppingCartStateMachine.TestCase
```
## API Fuzz Testing
### Schema-Driven Fuzzing
```python
# Generate fuzz inputs from OpenAPI/JSON Schema
import schemathesis
import hypothesis
# Load API schema and generate test cases
schema = schemathesis.from_url('[reference URL]')
@schema.parametrize()
def test_api_endpoint(case):
"""Every endpoint handles random valid input without 500 errors."""
response = case.call()
# Property: server should never return 500 for valid schema input
assert response.status_code < 500, (
f"Server error on {case.method} {case.path}\n"
f"Input: {case.body}\n"
f"Response: {response.text[:500]}"
)
# Target specific endpoints
@schema.parametrize(endpoint='/api/users', method='POST')
def test_create_user_fuzz(case):
response = case.call()
if response.status_code == 201:
data = response.json()
assert 'id' in data
assert 'email' in data
# Run with: pytest --hypothesis-seed=0 -v
# Schemathesis also has a CLI:
# schemathesis run [reference URL] --checks all
```
### Boundary Value Fuzzing
```python
# Systematic boundary value generation for API parameters
BOUNDARY_STRINGS = [
'', # Empty
' ', # Whitespace
' \t\n\r ', # Mixed whitespace
'a' * 1, # Minimum length
'a' * 255, # Common VARCHAR limit
'a' * 256, # One past VARCHAR limit
'a' * 65536, # Large string
'\x00', # Null byte
'\x00abc\x00', # Embedded null bytes
'null', # Literal "null"
'undefined', # Literal "undefined"
'true', # Boolean-like string
'0', # Numeric string
'-1', # Negative numeric string
'1.7976931348623157e+308', # Max float as string
'<script>alert(1)</script>',# XSS payload
"'; DROP TABLE users; --", # SQL injection
'{{7*7}}', # Template injection
'../../../etc/passwd', # Path traversal
'[reference URL]', # URL in text field
]
BOUNDARY_INTEGERS = [
0, -1, 1,
-2147483648, 2147483647, # int32 boundaries
-2147483649, 2147483648, # Just past int32
-9223372036854775808, # int64 min
9223372036854775807, # int64 max
]
BOUNDARY_NUMBERS = [
0.0, -0.0,
0.1 + 0.2, # Floating point precision
float('inf'), float('-inf'),
float('nan'),
1e-308, # Smallest positive float
1.7976931348623157e+308, # Largest float
]
def fuzz_endpoint(client, method, path, field_name, values):
"""Test an endpoint with a list of boundary values for a specific field."""
results = []
for value in values:
body = {field_name: value}
response = getattr(client, method)(path, json=body)
results.append({
'input': repr(value),
'status': response.status_code,
'body': response.text[:200],
})
# No endpoint should return 500 for any input
assert response.status_code < 500, (
f"500 error with {field_name}={repr(value)}: {response.text[:500]}"
)
return results
```
## Mutation Testing
### Concept
```
Mutation testing answers: "How good are my tests, really?"
Process:
1. Take your passing test suite
2. Make a small change (mutation) to your source code
3. Run the test suite against the mutated code
4. If tests still pass → SURVIVED MUTANT (tests missed this case)
5. If tests fail → KILLED MUTANT (tests caught this case)
Mutation score = killed mutants / total mutants * 100%
> 80%: Good test suite
> 90%: Excellent test suite
< 60%: Tests have significant gaps
Common mutation operators:
Ver no GitHub