Skip to content

Fix DNS rebinding TOCTOU bypass in validate_restricted_url#21591

Open
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin1/oss-7874-fix-dns-rebinding-toctou-bypass-in-validate_restricted_url
Open

Fix DNS rebinding TOCTOU bypass in validate_restricted_url#21591
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin1/oss-7874-fix-dns-rebinding-toctou-bypass-in-validate_restricted_url

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Closes OSS-7874.

validate_restricted_url() previously resolved a hostname once via socket.gethostbyname(), checked if the returned IP was private, then passed the original hostname to httpx. httpx re-resolves DNS at connection time, creating a TOCTOU window: an attacker-controlled DNS server can return a public IP during validation and a private IP during the actual connection, bypassing the SSRF check when allow_private_urls=False. gethostbyname also only considers the first A record, so a DNS response mixing public and private records could already bypass validation.

This PR closes both windows:

  • Hardens validate_restricted_url to use socket.getaddrinfo so every resolved address is checked (defends against mixed public/private A/AAAA records).
  • Adds SSRFProtectedAsyncHTTPTransport / SSRFProtectedHTTPTransport — httpx transports that wrap the httpcore network backend. On each TCP connect they resolve the hostname, reject private addresses, and pass the validated IP literal to the underlying backend so it cannot re-resolve DNS. TLS SNI is preserved because httpcore passes the original hostname to start_tls independently of the host used for the TCP connection.
  • Webhook and CustomWebhookNotificationBlock now use the protected transports whenever allow_private_urls=False, in addition to the pre-flight validate_restricted_url check.
Verification
  • tests/utilities/test_urls.py adds:
    • TestValidateRestrictedUrlMultiRecord — mixed public/private addresses from getaddrinfo are now rejected.
    • TestSSRFProtectedTransportTOCTOU — the transport rejects private IPs at connect time (simulates DNS rebinding), including the multi-record case, unresolvable hosts, and private IP literals; both async and sync transports are covered.
    • TestSSRFProtectedBackendPinsIP — confirms the wrapper backend connects to the validated IP, not the original hostname, so the underlying backend cannot re-resolve DNS.
  • Existing tests/blocks/test_webhook.py and tests/blocks/test_notifications.py continue to pass.
  • uv run ruff check and uv run ruff format --check pass.

Checklist

  • This pull request references any related issue by including "closes <link to issue>"
    • If no issue exists and your change is not a small fix, please create an issue first.
  • If this pull request adds new functionality, it includes unit tests that cover the changes
  • If this pull request removes docs files, it includes redirect settings in mint.json.
  • If this pull request adds functions or classes, it includes helpful docstrings.

Link to Devin session: https://app.devin.ai/sessions/0b65f4db71f04387be5945d92cce8ed5
Requested by: @desertaxle

Prior SSRF protection resolved a hostname with socket.gethostbyname(),
checked privateness of the returned IP, then passed the hostname to
httpx which re-resolved DNS at connection time.  An attacker-controlled
DNS server could return a public IP for the validation lookup and a
private IP for the actual connection, bypassing the check.

- Hardens validate_restricted_url() to use socket.getaddrinfo() so every
  resolved address is validated (defends against mixed public/private
  A/AAAA responses).
- Adds SSRFProtectedAsyncHTTPTransport and SSRFProtectedHTTPTransport,
  httpx transports that wrap the httpcore network backend; on each
  connect they resolve the hostname, reject private addresses, and pass
  the validated IP literal to the underlying backend so it cannot
  re-resolve DNS.
- Uses the protected transports in Webhook and
  CustomWebhookNotificationBlock when allow_private_urls=False.

Refs OSS-7874.

Co-authored-by: Alexander Streed <alex.s@prefect.io>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@desertaxle desertaxle changed the title Fix DNS rebinding TOCTOU bypass in validate_restricted_url Fix DNS rebinding TOCTOU bypass in validate_restricted_url Apr 17, 2026
@desertaxle desertaxle added the fix A fix for a bug in an existing feature label Apr 17, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Apr 17, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing devin1/oss-7874-fix-dns-rebinding-toctou-bypass-in-validate_restricted_url (a080abd) with main (4584ca8)

Open in CodSpeed

@desertaxle desertaxle marked this pull request as ready for review April 17, 2026 20:16
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2bad07afa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/prefect/utilities/urls.py Outdated
Comment on lines +262 to +265
resolved = _validate_resolved_hostname(host)
except _RestrictedHostError as exc:
raise httpcore.ConnectError(f"Refusing to connect to {host!r}: {exc}") from None
return resolved[0]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Try all validated DNS answers before failing connection

_resolve_and_validate_for_connect returns only resolved[0], so the protected transports attempt a single IP and give up even when later DNS answers are valid and reachable. This is a regression from the previous hostname-based connect path (which could fall back across A/AAAA answers), and it will cause false connection failures for dual-stack domains in single-stack environments (for example, first AAAA unreachable but A reachable). Consider retrying through all validated addresses instead of pinning only the first one.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in a080abd. _resolve_and_validate_for_connect now returns every validated IP, and the async/sync backends iterate them in order, retrying on ConnectError / ConnectTimeout / OSError and re-raising the last error only if all addresses fail. Added tests for the fallback-on-first-failure and all-fail-raises-last-error paths.

Previously the protected backends pinned a single resolved IP, which
broke dual-stack hostnames in single-stack environments: if the first
resolved address was unreachable (e.g. AAAA in an IPv4-only network)
the connection would fail even when a later A/AAAA record was
reachable.  Now iterate every validated IP, retry on ConnectError /
ConnectTimeout / OSError, and re-raise the last error only if all
addresses fail.

Addresses the Codex P1 review comment on #21591.

Co-authored-by: Alexander Streed <alex.s@prefect.io>
Co-Authored-By: alex.s <ajstreed1@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix A fix for a bug in an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant