Skip to content

harden(egress): SSRF defenses for url type, always-block cloud metadata, manifest lint#8

Open
jaschadub wants to merge 1 commit into
mainfrom
harden/url-egress-ssrf
Open

harden(egress): SSRF defenses for url type, always-block cloud metadata, manifest lint#8
jaschadub wants to merge 1 commit into
mainfrom
harden/url-egress-ssrf

Conversation

@jaschadub

Copy link
Copy Markdown
Contributor

Summary

Follow-up to the scope_target hardening. Extends SSRF defenses across the whole egress surface, scoped by the rule "if the command connects to the target, default-deny internal."

1. url validator hardened (the priority gap)

curl/wget-style fetchers are the prime SSRF vector, and validate_url previously had no internal-block or IP-canonicalization — it passed http://169.254.169.254/latest/meta-data/ and http://0x7f000001/. Now the URL host gets the same hygiene as scope_target: canonical-IP-only host, reject obfuscated IP-literal hosts, reject punycode hosts, and apply the egress IP policy to literal-IP hosts.

2. Always-block cloud metadata / link-local

169.254.0.0/16 and fe80::/10 (incl. v4-mapped) are now blocked for scope_target and url regardless of block_internal. IMDS credential theft has maximal blast radius and ~zero legitimate egress use, so it shouldn't depend on a flag. Loopback/private remain gated by block_internal.

3. Manifest lint (enforce, don't rely on memory)

A manifest whose command invokes a known egress binary (curl, wget, nmap, nc, masscan, ssh, …) must set block_internal on its scope_target/url args, or it is refused at load. Turns a silent unsafe default into a load-time failure.

4. Scope the flags

block_internal = true on the nmap_scan and curl_fetch examples (they connect to the target). whois/dig stay off — they take the target as a lookup key, not a connection endpoint.

Limitation (documented)

String-level IP checks catch literal internal IPs but not DNS rebinding (a public hostname resolving to 169.254.169.254 at connect time). The durable fix is resolve-then-check at connect time in the executor / an egress proxy; block_internal is defense-in-depth.

Testing

+4 tests (metadata-always, url host hardening, egress-lint pass/fail). 91 pass; cargo clippy --all-targets clean. Shared enforce_ip_policy() / always_blocked_ip_reason() helpers.

…nifest lint

Extends the scope_target hardening across the egress surface:

- url validator: apply scope_target's host hygiene to curl/wget-style fetchers
  (the prime SSRF vector) — canonical-IP-only host, reject obfuscated IP-literal
  hosts (0x7f000001 / 2130706433), reject punycode hosts, and apply the egress
  IP policy to literal-IP hosts.
- always-block link-local / cloud metadata (169.254.0.0/16, fe80::/10, incl.
  v4-mapped) for scope_target AND url, regardless of block_internal — IMDS
  credential theft has maximal blast radius and ~zero legitimate egress use.
- manifest lint: a manifest whose command invokes a known egress binary
  (curl/wget/nmap/nc/...) must set block_internal on its scope_target/url args,
  or it is refused at load — turns a silent unsafe default into a CI failure.
- set block_internal=true on the nmap_scan and curl_fetch examples (+ the nmap
  test fixture) to satisfy the new lint.

Shared enforce_ip_policy()/always_blocked_ip_reason() helpers. +4 tests
(metadata-always, url hardening, egress lint pass/fail); 91 pass, clippy clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant