Security probes¶
Five endpoints designed to look like things a gateway should refuse to forward. The handlers themselves are inert — they never fetch a URL, never escalate privileges, never emit smuggling-shaped responses. Their value is the shape they present so a gateway URL-filter, path-filter, or response-header limiter can pattern-match and refuse.
A correctly-configured gateway never forwards these requests; the api-test server only sees them if the gateway is mis-configured or missing a rule. Reaching api-test is the failure signal. If the audit log records a security probe, the gateway's filter coverage missed it.
| Method | Path | Probe shape | Gateway should... |
|---|---|---|---|
GET |
/v1/security/admin/secret |
Privileged-looking path | Refuse on path-filter. |
GET |
/v1/security/fetch?url=... |
SSRF-shape query parameter | Refuse when url points at localhost / link-local / non-allowlist. |
GET |
/v1/security/big-headers |
~32 KiB of response headers | Reject or rewrite per RFC 7230 §3.2.5. |
POST |
/v1/security/redirect-to?url= |
Open-redirect shape (status 200 + custom X-Would-Redirect-To) |
Refuse when url is unrestricted. |
GET |
/v1/security/control-chars?q= |
Control bytes in query | Sanitize, strip, or pass through observably. |
Every probe returns 200 OK when the input is well-formed (the
two query-driven probes return 400 only on a missing query value).
This is deliberate: api-test isn't supposed to judge the request —
the gateway is. A 200 from api-test means the gateway forwarded; the
gateway's own response (and whether the request reached api-test at
all) is the assertion surface.
/v1/security/admin/secret¶
Privileged-looking path. The handler returns a 200 with a self-aware JSON body:
{
"message": "This endpoint exists to be a probe target. A correctly-configured gateway should have refused to forward this request before it reached api-test."
}
What to assert: the audit log should contain no rows for this
path. If it does, your gateway is missing a path-based deny rule for
/admin/* or equivalent.
/v1/security/fetch?url=...¶
SSRF-shape query parameter. The handler does not fetch — it echoes the URL back:
The would_have_fetched field is a contract: it is always false.
A gateway running this probe can rely on the fact that api-test never
performs egress as a result of this call. Any "the upstream made a
callback" signal in your monitoring during a fetch test is therefore
a gateway bug (unexpected egress), not an artifact of the fixture.
Missing url: returns 400 with {"error": "url must be
provided (this endpoint is a probe; it does not actually fetch)"}.
What to assert: the audit log should contain no rows for
url=http://169.254.169.254/..., url=http://localhost/...,
url=http://127.0.0.1/..., or url=file:///.... If any do appear, the
gateway is missing SSRF allowlist heuristics.
/v1/security/big-headers¶
Emits 64 response headers named X-Big-Probe-0 through
X-Big-Probe-63, each with a 512-byte value — about 32 KiB of header
data total. Response body summarizes:
What to assert: the gateway should either reject the response (per RFC 7230 §3.2.5 server limits) or rewrite/strip the oversized headers before passing them to the client. A gateway with no header-size enforcement will forward all 64 headers verbatim, which the client (and many intermediate proxies) won't tolerate.
Two failure modes worth distinguishing:
- Request-side limit — the gateway never reaches api-test because it rejected the outbound response when it tried to read it. You'll see no audit row for the response on the gateway side, and a completed audit row on api-test's side.
- Response-side limit — the gateway forwarded the request, got the fat response, and refused to pass it on. You'll see audit rows on both sides; the gateway's response to the client is the rejection.
/v1/security/redirect-to?url=...¶
Open-redirect shape, but inert. Returns:
HTTP/1.1 200 OK
X-Would-Redirect-To: http://evil.example.com/phish
Content-Type: application/json
{ "would_redirect_to": "http://evil.example.com/phish" }
Note the careful design choices:
- Status is 200, not 3xx — no browser auto-follow, no redirect loop possible.
- The URL is in
X-Would-Redirect-To, notLocation— gateway URL-filters that scan response headers can still pattern-match the shape, butLocation-only checkers won't see anything. That asymmetry is itself a useful finding about the gateway's coverage. - CodeQL's
go/unvalidated-url-redirectionrule traces user input intoLocation-typed sinks; the custom header avoids the sink, so the static analyzer stays clean without per-file suppressions.
Missing url: returns 400 with {"error": "url must be
provided"}.
What to assert: the audit log should contain no rows for
url= values pointing at unrelated domains. A gateway that
sanitizes redirect targets (allowlist, same-origin, etc.) should
refuse this probe.
/v1/security/control-chars?q=...¶
Echoes the query parameter back, with byte counts and a control-character flag:
has_control is true when any byte in q is below 0x20 (the C0
control range) or equal to 0x7f (DEL). Newline (0x0a), carriage
return (0x0d), tab (0x09), and null (0x00) all qualify.
What to assert: compare the q value api-test recorded against
the bytes the client originally sent through the gateway. Three
possible gateway behaviors:
- Sanitizes — the gateway strips control bytes; api-test sees a
shorter
qthan the client sent. Recommended for most use cases. - Pass-through — the gateway forwards the bytes verbatim;
byte_countandhas_controlmatch the client's input. Acceptable if the gateway documents this. - Rejects — the gateway refuses the request entirely; api-test sees no row at all. Most defensive option.
Pass-through is the silent risk: a downstream system that's not expecting control bytes may interpret them as line terminators or escape sequences, opening request-splitting or log-injection issues.
Why probes return 200, not 400/403¶
Returning a hard error from api-test would muddy the assertion. We
want the gateway's behavior to be the observable, not the upstream's
opinion. A 200 with a self-descriptive body says: "I'm a probe, you
should never have reached me." The 400s on missing url are about
contract correctness, not security policy.
CodeQL compatibility¶
All probes are designed to pass CodeQL security-and-quality
analysis without per-file suppressions:
- The
fetchhandler never invokes an HTTP client. - The
redirect-tohandler never writes user input toLocationor useshttp.Redirect. - The
big-headersvalue is a fixed pattern (strings.Repeat), not user-controlled. - The
control-charsecho is a JSON-encoded string (so control bytes are escaped on the wire, not re-emitted as raw bytes).
This matters because the alternative — a fixture that legitimately implements an SSRF or an open redirect — would either need ongoing suppression maintenance or constant exception filings against CodeQL. The inert-probe design sidesteps both.