net-loopback-ssrf
Refuses loopback targets (SSRF)
The sections below are generated from the CLI (methodology 0.3-draft, CLI 0.3.0); the same text is printed by sealmark explain net-loopback-ssrf.
- pack
net- weight
- critical
- scope
- server (does a URL tool refuse loopback addresses? tested against a harness-owned loopback listener)
- decided by
- listenerresponse
- since
- 0.2-draft (methodology 0.3-draft)
- config keys
checks.net-loopback-ssrf.corpus.addchecks.net-loopback-ssrf.corpus.removechecks.net-loopback-ssrf.enabledchecks.net-loopback-ssrf.severitynet.allow_dnsnet.control_url
Probes
loopback127.0.0.1localhostlocalhostipv6[::1]short127.1 (short form)hex0x7f000001 (hex)decimal2130706433 (decimal)unspecified0.0.0.0ipv4-mapped[::ffff:127.0.0.1] (IPv4-mapped IPv6)redirectloopback URL that answers 302 to another loopback URLdns127.0.0.1.nip.io (only with net.allow_dns: true)twina data: URL with no host (sent only when every loopback probe was refused and no control URL shows the tool fetches); its reply must differ from the loopback refusals
Settings and thresholds
Every check also takes checks.net-loopback-ssrf.enabled and checks.net-loopback-ssrf.severity; they are described once in the configuration reference.
| Threshold | Default | Set with | Standard |
|---|---|---|---|
allow_dns | false | net.allow_dns | locked |
control_url | none | net.control_url | locked |
| Key | Default | Standard | Meaning |
|---|---|---|---|
checks.net-loopback-ssrf.corpus.add | [] | locked | Extra probes for net-loopback-ssrf, validated against the pack's corpus schema. Appends across layers. Any addition makes the run CUSTOM. |
checks.net-loopback-ssrf.corpus.remove | [] | locked | Probe ids or exact strings to drop from the net-loopback-ssrf corpus. Appends across layers. Any removal makes the run CUSTOM. |
net.allow_dns | false | locked | Allow probes that need DNS resolution (127.0.0.1.nip.io). |
net.control_url | none | locked | A URL the harness may fetch through the server as a control, so a no-hit, no-refusal result can pass instead of staying inconclusive. |
How the verdict is established
- Scoring. critical and scored: pass = 1 point, part = 0.5, fail = 0. A fail caps the pack's grade at D, whatever the other checks scored.
What to fix
Check the address after the URL is parsed and normalised, and again on every redirect, and refuse loopback, link-local and private addresses in every spelling (dotted, localhost, ::1, short, hex, decimal, 0.0.0.0, IPv4-mapped IPv6 such as [::ffff:127.0.0.1]). Say what was refused: a refusal ties the decline to the target or to the rule (it names loopback, private, internal, reserved or not-routable, says the URL or host is blocked or not allowed, says only public hosts may be fetched, or names an egress policy or an unsupported scheme) and is returned as an error result (isError) or a JSON-RPC -32602. A reply that only reports a failed fetch (proxy, socket, DNS, timeout, "refused to connect", "connection blocked") is not read as a refusal, and a tool that cannot fetch anything is inconclusive, not passed. The harness also sends one URL with no host (a data: URL) to a tool whose loopback probes were all refused and nothing else was observed: a tool that answers it with the same sentence, or with address wording for a URL that has no address, has one answer for everything, so the refusals describe nothing and the check is inconclusive. Give a hostless URL its own reply (a scheme refusal or "URL must have a host"). A guard that really does answer every rejection with one constant sentence stays inconclusive until you set net.control_url to a URL the tool can reach, so the harness can see it fetch. A URL tool may relay the probe values to its own upstream; keep a tool out of the net pack with tools.exclude.
False positives
A server that is meant to fetch local services (a developer tool) fails by design; it should say so and be run with the target outside the loopback range. A response counts as a refusal only when it ties the refusal to the target or names the rule, and the listener saw nothing. Two phrase families count. The rule: it names the rule that refused (SSRF, unsafe destination, resolves to a private or loopback address, only public hosts or only http(s) allowed, egress policy, failed validation, an unsupported scheme). The address class: it names the class of address (loopback, link-local, private, internal or reserved address, local network, public hosts, non-routable, not public), unless the reply is also a failure report and the clause with the noun does not refuse. Beyond those, a refusal word (blocked, not allowed, forbidden, denied, rejected, restricted, cannot access) next to a word for the target (host, address, URL, destination, domain, ip), or a validation error that names the loopback address (JSON-RPC -32602, or an isError reply that says invalid, bad request or a refusal word). A message that merely echoes the URL, or the host and the listener's port, does not count. The evidence line names the tier that decided.
False negatives
Spellings outside the corpus (the corpus has one entry per normalisation family: dotted, name, IPv6, short, hex, decimal, unspecified and IPv4-mapped IPv6; octal and the rest of 127/8 are not tried), DNS names that resolve to loopback (unless net.allow_dns), DNS rebinding, redirects from a public host to loopback, link-local and private ranges (never sent). A tool that fails for an unrelated reason is not read as refusing: a reply that only reports a failed fetch (refused to connect, connection blocked by a proxy, a socket or DNS error, a timeout, ENETUNREACH, a certificate error) is a failure whatever address words it uses, and so is a bare "blocked" or "restricted" with no word for the target. Words alone cannot show that a tool fetches anything, so when every loopback probe was refused and no control URL shows the tool fetches, the harness sends one more call, a data: URL with no host (the twin): a tool that gives it the same reply word for word, or that refuses it in wording about an address, refuses everything and is inconclusive; a refuse-everything tool whose reply to the hostless URL differs from its loopback refusals passes on text, with the evidence line saying that no fetch of any URL was shown to work, and net.control_url (a URL the tool can reach) is the way to show it fetches. Unless a reply ties its refusal to the target, the check is inconclusive until net.control_url shows the tool can fetch (the only target the harness owns is the one a guarded tool refuses, so only the tool's own words, or a control URL you supply, can tell a refusal from a failure). The server must be able to reach this machine's loopback listener (stdio, or an HTTP server on this machine).