Skip to content

fix(fetch_url): optionally trust fake-ip placeholder ranges to avoid SSRF false-positives#2355

Open
h3c-hexin wants to merge 1 commit into
Hmbown:mainfrom
h3c-hexin:pr2/fetch-url-fakeip-trust
Open

fix(fetch_url): optionally trust fake-ip placeholder ranges to avoid SSRF false-positives#2355
h3c-hexin wants to merge 1 commit into
Hmbown:mainfrom
h3c-hexin:pr2/fetch-url-fakeip-trust

Conversation

@h3c-hexin
Copy link
Copy Markdown
Contributor

@h3c-hexin h3c-hexin commented May 29, 2026

Summary

The DNS-resolution SSRF guard in validate_dns_resolved_ip rejects any hostname that resolves into a restricted IP range. Under a transparent-proxy / TUN setup running in fake-ip mode, the local resolver maps every hostname into a reserved placeholder range (commonly 198.18.0.0/15), so the guard rejects all outbound fetch_url requests even though the proxy routes them correctly.

This adds a narrowly-scoped, opt-in trust list on NetworkPolicyDecider:

  • with_trusted_fakeip_cidrs(&["198.18.0.0/15"]) registers IPv4 CIDR ranges to treat as benign fake-ip placeholders. Default is empty — no behavior change unless the embedder configures it.
  • is_trusted_fakeip_addr(ip) matches only those configured ranges.

validate_dns_resolved_ip now allows a resolved IP if it is either in a configured fake-ip range or the host is on the existing trusted-proxy list. Real private / loopback / link-local / cloud-metadata IPs match neither and stay blocked, so this does not widen SSRF exposure for default configurations.

Includes IPv4 CIDR parse/containment helpers and a unit test covering placeholder-allowed / real-private-blocked / nothing-configured.

Testing

  • cargo fmt --all -- --check
  • cargo build -p codewhale-tui (compiles, 42s)
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant (trusted_fakeip_cidr_allows_placeholder_but_not_real_private)
  • Verified TUI behavior manually if UI changes (n/a)

Greptile Summary

This PR adds an opt-in mechanism to NetworkPolicyDecider that lets embedders declare IPv4 CIDR ranges as benign fake-IP placeholders, preventing the DNS-resolution SSRF guard from rejecting all outbound requests in transparent-proxy / TUN fake-ip setups. The default configuration is unchanged — no CIDR ranges are trusted unless explicitly registered.

  • with_trusted_fakeip_cidrs registers CIDR ranges via a builder pattern; is_trusted_fakeip_addr tests a resolved IP against them. Both are IPv4-only, consistent with how fake-IP is implemented in practice.
  • validate_dns_resolved_ip now allows a resolved IP if it matches a configured fake-IP range or the host is on the pre-existing trusted-proxy list; all other restricted IPs (loopback, private, link-local, cloud-metadata) continue to be blocked under default configuration.
  • A unit test covers placeholder-allowed, real-private-blocked, and nothing-configured cases.

Confidence Score: 4/5

Safe to merge for default deployments; the SSRF bypass is entirely opt-in and the CIDR arithmetic is correct.

The core logic is sound — CIDR masking is applied symmetrically, the prefix-0 edge case is handled before any shift, and IPv4-mapped IPv6 addresses are not affected by the new bypass. Both findings are observability/UX concerns rather than functional defects: invalid CIDR strings are silently dropped with no log, and the audit label is the same whether the IP-range path or the host-list path triggered the allow.

The with_trusted_fakeip_cidrs builder in network_policy.rs and the updated condition in fetch_url.rs are the only files that need a second look.

Important Files Changed

Filename Overview
crates/tui/src/network_policy.rs Adds CIDR parse/containment helpers, a trusted_fakeip_cidrs field, and a builder method + accessor on NetworkPolicyDecider. Logic is correct (prefix-0 edge case handled, mask applied symmetrically to both sides). Minor: invalid CIDRs are silently dropped in with_trusted_fakeip_cidrs with no log.
crates/tui/src/tools/fetch_url.rs Extends validate_dns_resolved_ip to allow IPs in configured fake-IP CIDR ranges in addition to the existing host-based trust check. The change is minimal and correct; audit label does not distinguish which condition triggered the bypass.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[fetch_url resolves hostname to IP] --> B{is_restricted_ip?}
    B -- No --> C[Allow request]
    B -- Yes --> D{decider configured?}
    D -- No --> E[Block: SSRF guard]
    D -- Yes --> F{is_trusted_fakeip_addr?\nIP in configured CIDR ranges}
    F -- Yes --> G[record_trusted_proxy_fakeip_allow\nAllow request]
    F -- No --> H{trusts_proxy_fakeip_host?\nHost on explicit proxy list}
    H -- Yes --> G
    H -- No --> E
Loading

Comments Outside Diff (1)

  1. crates/tui/src/tools/fetch_url.rs, line 398-403 (link)

    P2 Audit label doesn't distinguish IP-range bypass from host-list bypass

    record_trusted_proxy_fakeip_allow always writes "TrustedProxyFakeIp-Allow" regardless of which branch of the || triggered the allow. When the IP matches a configured CIDR (new path) vs. the host being on the explicit proxy trust list (old path), the audit log looks identical. This makes it hard to answer in post-incident analysis whether a bypass came from the new range-trust feature or the pre-existing host allowlist. Consider using a separate audit label (e.g. "TrustedFakeIpCidr-Allow") for the IP-range case.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "fix(fetch_url): optionally trust fake-ip..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

…SSRF false-positives

The DNS-resolution SSRF guard in `validate_dns_resolved_ip` rejects any
hostname that resolves into a restricted IP range. Under a transparent-proxy
/ TUN setup running in `fake-ip` mode, the local resolver maps *every*
hostname to a reserved placeholder range (commonly `198.18.0.0/15`), so the
guard rejects all outbound `fetch_url` requests even though the proxy will
route them correctly.

Add a narrowly-scoped, opt-in trust list on `NetworkPolicyDecider`:

- `with_trusted_fakeip_cidrs(&["198.18.0.0/15"])` registers IPv4 CIDR ranges
  to treat as benign fake-ip placeholders (default: empty — no behavior
  change unless the embedder configures it).
- `is_trusted_fakeip_addr(ip)` matches only those configured ranges.

`validate_dns_resolved_ip` now allows a resolved IP if it is either in a
configured fake-ip range or the host is on the existing trusted-proxy list.
Real private/loopback/link-local/cloud-metadata IPs match neither and stay
blocked, so this does not widen SSRF exposure for default configurations.
Includes CIDR parsing/containment helpers and a unit test covering the
placeholder-allowed / real-private-blocked / nothing-configured cases.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for configuring trusted IPv4 CIDR ranges as benign fake-IP placeholders (e.g., for TUN/transparent-proxy setups in fake-ip mode) to bypass restricted-IP SSRF blocks. It adds parsing and matching logic in NetworkPolicyDecider and integrates it into the DNS resolution validation. The feedback suggests improving is_trusted_fakeip_addr to handle IPv4-mapped IPv6 addresses, preventing bypass failures in dual-stack or IPv6-preferred environments.

Comment on lines +480 to 489
pub fn is_trusted_fakeip_addr(&self, ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => self
.trusted_fakeip_cidrs
.iter()
.any(|(base, prefix)| ipv4_in_cidr(*v4, *base, *prefix)),
// fake-ip placeholders are IPv4-only in practice.
IpAddr::V6(_) => false,
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In dual-stack or IPv6-preferred environments, DNS resolvers may return IPv4-mapped IPv6 addresses (e.g., ::ffff:198.18.0.5). While is_restricted_ip correctly unwraps these to check them as IPv4, is_trusted_fakeip_addr currently returns false for all IPv6 addresses. This will cause fake-IP bypasses to fail when mapped addresses are returned.

We should handle IPv4-mapped IPv6 addresses by extracting the inner Ipv4Addr using to_ipv4_mapped() before checking the CIDR ranges.

Suggested change
pub fn is_trusted_fakeip_addr(&self, ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => self
.trusted_fakeip_cidrs
.iter()
.any(|(base, prefix)| ipv4_in_cidr(*v4, *base, *prefix)),
// fake-ip placeholders are IPv4-only in practice.
IpAddr::V6(_) => false,
}
}
pub fn is_trusted_fakeip_addr(&self, ip: &IpAddr) -> bool {
let v4 = match ip {
IpAddr::V4(v4) => Some(*v4),
IpAddr::V6(v6) => v6.to_ipv4_mapped(),
};
if let Some(v4) = v4 {
self.trusted_fakeip_cidrs
.iter()
.any(|(base, prefix)| ipv4_in_cidr(v4, *base, *prefix))
} else {
false
}
}

Comment on lines +463 to +469
pub fn with_trusted_fakeip_cidrs(mut self, cidrs: &[&str]) -> Self {
for cidr in cidrs {
if let Some(parsed) = parse_ipv4_cidr(cidr) {
self.trusted_fakeip_cidrs.push(parsed);
}
}
self
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent CIDR skip makes misconfiguration invisible

Invalid CIDR strings passed to with_trusted_fakeip_cidrs are silently discarded. If an operator types "198.18.0.0-15" or "198.18.0.0/55" by mistake, every request that resolves into that range will continue to be blocked with no indication that the configuration was rejected. A tracing::warn! (or at minimum a debug log) when a CIDR fails to parse would make these silent drops visible in the existing audit log infrastructure.

Fix in Codex Fix in Claude Code Fix in Cursor

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