Skip to content

[Security / v0.5.3] Harden webhook SSRF guard against DNS rebinding (TOCTOU between pre-resolve and requests.post) #14

Closed

Description

@coderabbitai

Context

Raised in PR #13 (review comment: #13 (comment)) by @coderabbitai.

Requested by: @cemililik

Problem

_is_private_destination() in forgelm/webhook.py pre-resolves the webhook hostname via socket.getaddrinfo to block private/loopback destinations. However, requests.post() performs its own DNS resolution when making the actual connection, creating a TOCTOU window: an attacker-controlled DNS server could return a public IP on the first lookup (passing the check) and a private IP on the second lookup (when requests connects), bypassing the SSRF guard.

Existing mitigations (already in place)

  • allow_private_destinations: false is the default in WebhookConfig — operators must explicitly opt in to private destinations.
  • allow_redirects=False prevents redirect-chaining to a private destination after a valid initial response.
  • The attack surface is narrow: requires attacker control of the DNS server for the configured webhook domain.

Proposed fix direction

  1. Resolve the hostname to a concrete public IP inside _is_private_destination() (or a new helper) and return that IP alongside the boolean result.
  2. Reconstruct the webhook URL with the IP literal.
  3. Pass the original hostname in a Host header.
  4. For HTTPS, register requests_toolbelt.adapters.host_header_ssl.HostHeaderSSLAdapter on a per-call Session to preserve SNI and certificate validation against the original hostname.
  5. Keep allow_redirects=False.

Dependencies / complexity

  • Adds requests-toolbelt as a dependency (new hard dep or optional extra — TBD).
  • Requires careful SNI/cert validation testing across supported requests versions.
  • Should land in its own isolated commit/PR so it can be reverted cleanly if behaviour differs across requests releases.

Target

v0.5.3 hardening cycle. Deferred from v0.5.2 (Phase 12) to keep the HostHeaderSSLAdapter wiring reviewable in isolation.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions