75 lines
3.7 KiB
Markdown
75 lines
3.7 KiB
Markdown
# Adversarial API testing
|
|
|
|
Every request in this suite goes to the application under test. Nothing here probes a host service,
|
|
scans a network, or reaches anything ModelForge does not own.
|
|
|
|
## What is sent
|
|
|
|
```text
|
|
malformed bodies empty, truncated, non-JSON, bare scalars, NaN, Infinity, null bytes,
|
|
200-deep nesting
|
|
wrong types integers, nulls, arrays, objects and booleans where a string is required
|
|
oversized input 100 KB identifiers, 5 MB bodies, 5 KB enum values
|
|
mass assignment state, restore_eligible, verified_at, encryption_key, id
|
|
hostile strings traversal, encoded traversal, absolute and UNC paths, file:// and
|
|
link-local URLs, SQL, template and JNDI expressions, shell metacharacters,
|
|
script and image XSS payloads, bidi overrides, astral-plane and emoji runs
|
|
identifiers malformed UUIDs, SQL appended to a path segment, 500-character values
|
|
query bounds zero, negative, out-of-range, non-numeric and injected limits
|
|
headers CR/LF and null-byte injection into a correlation id
|
|
credentials none, empty, wrong scheme, wrong case, oversized, null-byte, injected
|
|
```
|
|
|
|
## What must happen
|
|
|
|
A rejected value and a stored-but-inert value are both acceptable. A 5xx is not: it means input
|
|
reached somewhere it should not have.
|
|
|
|
```text
|
|
400 / 422 malformed or wrongly typed, with a typed error envelope
|
|
403 authenticated but out of scope
|
|
404 well-formed identifier that does not exist
|
|
413 / 422 oversized
|
|
never 5xx for any input in the matrix
|
|
```
|
|
|
|
Every error response carries `code`, `message` and `correlation_id`, and nothing else: no
|
|
traceback, no SQL, no driver name, no DSN, no file path, no credential. This is asserted directly,
|
|
including with the database stopped — a dependency failure is exactly when a framework is most
|
|
likely to leak its internals.
|
|
|
|
## Scope isolation
|
|
|
|
The capability a credential names is matched exactly. Case changes, appended whitespace, a second
|
|
capability in the same string and zero-width characters do not widen it. A contract version is part
|
|
of the identity: a client scoped to `rag.embedding@1` cannot reach `rag.embedding@2`.
|
|
|
|
A client bound to a project may only serve that project's binding, even when its capability list
|
|
would otherwise allow more, and a deprecated binding stops serving immediately.
|
|
|
|
## No enumeration oracle
|
|
|
|
An unknown credential, a revoked one, a near-miss differing in the last character and an empty one
|
|
all produce the same status and the same code. A caller cannot learn whether a guess was close.
|
|
|
|
## Mass assignment
|
|
|
|
Request models are `extra="forbid"`, so a caller cannot write a field the contract never offered.
|
|
Attempting to set `state`, `restore_eligible`, `verified_at` or an `id` is a validation error rather
|
|
than a silently ignored field — silently ignoring is how these become exploitable later.
|
|
|
|
## Path handling
|
|
|
|
Backup identities become directory names, so the identity pattern refuses traversal outright rather
|
|
than relying on later sanitisation. Restore destinations are parsed and identifier-validated:
|
|
non-PostgreSQL schemes, SQL appended to a database name, an empty database and a null byte are all
|
|
refused. The M15 allowlisted-root resolution remains the second layer.
|
|
|
|
## A defect this suite found
|
|
|
|
Python's JSON parser accepts `NaN` and `Infinity`; its serialiser rejects them. The validation
|
|
handler echoed the rejected value straight back into the error response, so a body containing one
|
|
made the error response itself fail to serialise — turning a 422 into a server error, on input that
|
|
needs no credential to send. Rejected values are now rendered safely and truncated, since rejected
|
|
input is unbounded by definition.
|