Skip to content

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:

{
  "asked_for": "http://169.254.169.254/latest/meta-data/",
  "would_have_fetched": false
}

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:

{ "header_count": 64, "header_bytes": 33024 }

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, not Location — gateway URL-filters that scan response headers can still pattern-match the shape, but Location-only checkers won't see anything. That asymmetry is itself a useful finding about the gateway's coverage.
  • CodeQL's go/unvalidated-url-redirection rule traces user input into Location-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:

{
  "q": "helloworld",
  "byte_count": 11,
  "has_control": true
}

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 q than the client sent. Recommended for most use cases.
  • Pass-through — the gateway forwards the bytes verbatim; byte_count and has_control match 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 fetch handler never invokes an HTTP client.
  • The redirect-to handler never writes user input to Location or uses http.Redirect.
  • The big-headers value is a fixed pattern (strings.Repeat), not user-controlled.
  • The control-chars echo 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.