Files
pad/internal
xarmian 0aa3988319 feat(urlimport): URL fetcher with SSRF guard + content-type detection (TASK-1469) (#552)
* feat(urlimport): URL fetcher with SSRF guard + content-type detection (TASK-1469)

First slice of PLAN-1467's "Insert from URL" feature. Adds the
internal/urlimport package with:

- fetch.go: SSRF-guarded HTTP GET (10s timeout, 5 MB body cap, redirect
  re-validation, redacted-error formatting). Blocks loopback, RFC1918,
  CGNAT, IPv4/IPv6 link-local (incl. 169.254.169.254 cloud-metadata),
  IPv6 unique-local, and the unspecified address. Hostnames are
  resolved and every returned IP is checked.
- detect.go: Content-type + body-prefix sniff returning "openapi"
  (JSON or YAML, OpenAPI 3.x or Swagger 2.0) or "generic". Inspects
  at most 64 KiB.
- fetch_test.go: Table-driven SSRF tests covering 24 cases plus
  happy-path, size-cap, timeout, non-2xx, context-cancel, and a
  stubbed-transport redirect re-validation.
- detect_test.go: 20 detection cases including OpenAPI JSON, Swagger
  YAML, vendor media types, leading comments, indented-key negatives,
  and charset-parameter normalization.

Package name is urlimport (not "import" — reserved word). No callers
yet; the endpoint that consumes Fetcher + Detect lands in TASK-1472.

Parent: PLAN-1467.

* fix(urlimport): close DNS-rebinding gap + handle >64 KiB OpenAPI JSON per Codex review (round 1)

- HIGH: Add safe dialer transport (newSafeTransport). ValidateURL no
  longer does DNS — the dialer resolves once and validates the
  resolved IP at dial time, then dials that exact IP. DNS rebinding
  can no longer slip a public-IP validation past a loopback fetch.
  ValidateURL becomes a pre-flight (scheme/credentials/IP-literal
  only) with the canonical guarantee now at the transport layer.

- MEDIUM: For JSON bodies over the 64 KiB sniff cap, switch from
  full Unmarshal (which fails on a truncated tail) to a streaming
  json.Decoder scan that walks top-level keys and short-circuits as
  soon as `openapi` or `swagger` is seen. Real-world specs over 64
  KiB are now classified correctly, including the case where the
  `openapi` key is not the first top-level entry.

Tests added:
- TestFetch_DialerBlocksLoopbackHostname (dial-time rebinding guard)
- TestDetect_LargeOpenAPIJSON (>200 KiB OpenAPI body, key first)
- TestDetect_LargeOpenAPIJSON_KeyNotFirst (openapi key after huge info)
- TestDetect_LargeJSONNotOpenAPI (huge non-OpenAPI stays generic)

Removed the DNS-resolution case from TestValidateURL's notes and
added a positive case proving hostnames pass the pre-flight (the
dial-time check is now the canonical guard).

* fix(urlimport): disable env proxy and reuse safe transport per Codex review (round 2)

- HIGH: Set Proxy=nil on the safe transport. ProxyFromEnvironment
  would route via HTTP_PROXY/HTTPS_PROXY, where the dialer connects
  to the proxy host instead of the target — silently bypassing the
  hostname-resolution SSRF check inside DialContext. Operators who
  need an outbound proxy can wire their own trusted transport into
  Fetcher.Transport.

- MEDIUM: Memoize the default safe transport per Fetcher via
  sync.Once. Previously each Fetch built a fresh *http.Transport
  whose keep-alive idle-pool stayed in scope until GC, leaking
  FDs under repeated imports. Now one transport is shared by all
  Fetch calls on a Fetcher; AllowLocal is captured at first use.
2026-05-14 23:51:44 -04:00
..
2026-03-26 01:52:36 +00:00