Skip to content

SSRF: fetch guard is literal-only — a hostname resolving to a blocked IP bypasses it #206

Description

@spartan8806

Hi! Really enjoying wigolo. Following the "open an issue first for non-trivial changes" guidance before proposing anything.

The gap: guardFetchUrl / guardServeTarget validate only the literal hostname — which the code already notes ("we never actually resolve here… a follow-up tier could re-check at fetch time"). Since the fetch layer resolves DNS itself, a public hostname whose A/AAAA record points at a blocked address — cloud metadata (169.254.169.254), an RFC-1918 host, or a loopback service in serve mode — passes the guard and is only reached after resolution. That defeats the "always block metadata" and serve-mode loopback protections.

Proposed approach: add guardResolvedHost(host, field, { allowPrivate, lookup }) that resolves the host and runs every resolved IP back through your existing guardFetchUrl (as an http:/// literal) — so the resolved-IP policy is identical to the literal one, no drift. Wire it into the fetch path (http-client redirect loop first; then the dispatch / firecrawl / browser seams). lookup is injectable so it's unit-testable without real DNS.

Scope: this closes the static-record bypass (the metadata case). Full rebinding (TOCTOU) safety also wants the socket pinned to the validated IP (a lookup hook / custom dispatcher) — I'd suggest the resolved-IP re-check first (small, high-value), pinning as a follow-up.

I've got a focused branch + tests ready to PR if this direction works for you — happy to adjust first. Does this approach sound right, or would you rather pin at the connector level from the start?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions