Skip to content

fix(aiohttp): block private/metadata IPs in api_base to close SSRF gap from PR #26264 (CWE-918) - #28246

Closed
DrishnaTrivedi wants to merge 3 commits into
BerriAI:litellm_internal_stagingfrom
DrishnaTrivedi:fix/aiohttp-ssrf-private-ip-block
Closed

fix(aiohttp): block private/metadata IPs in api_base to close SSRF gap from PR #26264 (CWE-918)#28246
DrishnaTrivedi wants to merge 3 commits into
BerriAI:litellm_internal_stagingfrom
DrishnaTrivedi:fix/aiohttp-ssrf-private-ip-block

Conversation

@DrishnaTrivedi

@DrishnaTrivedi DrishnaTrivedi commented May 19, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Closes SSRF gap in aiohttp_handler.py (CWE-918, findings #13–14).

Summary

PR #26264 blocked private IPs in the main HTTP handler but aiohttp_handler.py was not
covered. User-controlled api_base is passed directly to session.post(url=api_base, ...) at
line ~196 without IP validation, allowing requests to 169.254.169.254 (AWS IMDS),
10.x.x.x internal hosts, loopback, or CGNAT addresses.

Fix

Added _assert_not_private_url() which resolves the hostname via socket.getaddrinfo() and
checks against RFC-1918 / link-local / loopback / CGNAT networks before the request is made.
Called once per invocation of _make_common_async_call(), before the retry loop.

Networks blocked

  • 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC-1918)
  • 169.254.0.0/16 (link-local / AWS IMDSv1)
  • 127.0.0.0/8 (loopback)
  • 100.64.0.0/10 (CGNAT)
  • ::1/128, fc00::/7 (IPv6 loopback / ULA)

DNS failures are not blocked — the request fails naturally with a connection error.

  • Added 9 tests in tests/test_litellm/llms/test_aiohttp_ssrf_protection.py
  • All tests pass (uv run pytest tests/test_litellm/llms/test_aiohttp_ssrf_protection.py -v)
  • uv run black . — no changes needed
  • uv run ruff check — all checks passed
  • Scope isolated — only aiohttp_handler.py SSRF guard + test

Type

🐛 Bug Fix


What changed: Added _assert_not_private_url() to aiohttp_handler.py + 9 tests (all passing).
The key improvement over the playbook: the ValueError-inside-try bug was fixed so private-IP
errors and DNS failures are handled separately.

… from BerriAI#26264 (CWE-918)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes an SSRF gap in aiohttp_handler.py (CWE-918) that was missed when PR #26264 patched the main HTTP handler. It introduces an IP blocklist, a preflight _assert_not_private_url() check on both async and sync call paths, and a custom aiohttp resolver (_SSRFGuardResolver) that validates DNS answers at TCP-connection time to eliminate the TOCTOU DNS-rebinding window.

  • _BLOCKED_NETWORKS covers RFC-1918, loopback, link-local (including 0.0.0.0/8), CGNAT, and IPv6 ULA/link-local ranges; _is_blocked_address correctly unwraps IPv4-mapped IPv6 addresses before checking.
  • _SSRFGuardResolver hooks into aiohttp's TCPConnector so every connection — including redirect targets — is validated at the network layer, not just as a pre-flight; all DNS answers are checked to prevent A-record rotation attacks.
  • Nine new mock-only tests cover all blocked ranges and edge cases; asyncio.get_event_loop() in both the resolver and test helpers should be replaced with asyncio.get_running_loop() / asyncio.run() to avoid Python 3.10+ deprecation warnings.

Confidence Score: 5/5

The change is well-scoped to the aiohttp handler's SSRF guard and does not touch any other request path or authentication logic; safe to merge.

The SSRF mitigation logic is correct and thorough — all DNS answers are validated, IPv4-mapped IPv6 is unwrapped, 0.0.0.0/8 and fe80::/10 are included, and the custom resolver closes the TOCTOU window. The only issues are stylistic and do not affect runtime correctness on supported Python versions.

No files require special attention; both changed files are isolated to the SSRF guard implementation and its tests.

Important Files Changed

Filename Overview
litellm/llms/custom_httpx/aiohttp_handler.py Adds _BLOCKED_NETWORKS, _is_blocked_address, _assert_not_private_url, and _SSRFGuardResolver to close the SSRF gap; the resolver uses deprecated asyncio.get_event_loop() inside an async method (should be get_running_loop()).
tests/test_litellm/llms/test_aiohttp_ssrf_protection.py New test file with 9 mock-only tests covering the SSRF guard and custom resolver; uses deprecated asyncio.get_event_loop().run_until_complete() pattern that emits DeprecationWarnings in Python 3.10+ and will break in future Python versions.

Reviews (5): Last reviewed commit: "fix(aiohttp): add SSRFGuardResolver to c..." | Re-trigger Greptile

Comment thread litellm/llms/custom_httpx/aiohttp_handler.py Outdated
Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.41667% with 19 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/llms/custom_httpx/aiohttp_handler.py 60.41% 19 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
@veria-ai

veria-ai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

PR overview

High: Redirects can bypass the SSRF guard

This PR adds private-IP checks before aiohttp/httpx calls and a guarded aiohttp resolver. Redirects are still followed by default, so an attacker-controlled public api_base can redirect the request to a private or metadata IP after the initial URL check has passed.

Security review

  • 1 new security issue(s) were flagged in the latest review.
  • 2 issue(s) remain open on this pull request.

Risk: 8/10

Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
Comment thread litellm/llms/custom_httpx/aiohttp_handler.py Outdated
@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 2/5

Why blocked:

  • 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.) (pr_related_failures, -2 pts)
  • Greptile 3/5 (greptile_low, -1 pts)

Details: Score docked for: 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.); Greptile 3/5.

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

…t block, IPv4-mapped IPv6, fe80+0.0.0.0

- Check all getaddrinfo answers (not just [0]) to prevent A-record rotation bypass
- Add _assert_not_private_url to sync path (_make_common_sync_call)
- Pass allow_redirects=False to session.post to prevent redirect-based bypass
- Unwrap IPv4-mapped IPv6 addresses (::ffff:10.x.x.x) before network check
- Add 0.0.0.0/8 and fe80::/10 (IPv6 link-local) to blocked networks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
if not hostname:
return
try:
answers = socket.getaddrinfo(hostname, None)

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: DNS rebinding can bypass the private-IP check

This preflight resolves the hostname once, but the actual aiohttp/httpx request resolves it again later. A caller who controls api_base can use a domain that returns a public address for this check and then a private or metadata address for the client connection; enforce the block in the client resolver/transport at connect time, or pin the request to the vetted resolved address.

@DrishnaTrivedi

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 2/5

Why blocked:

  • 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.) (pr_related_failures, -2 pts)
  • Greptile 3/5 (greptile_low, -1 pts)

Details: Score docked for: 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.); Greptile 3/5.

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

… restore redirects

Replace allow_redirects=False with _SSRFGuardResolver (aiohttp.abc.AbstractResolver)
that validates every resolved IP at TCP-connection time — covering redirect targets too.
This eliminates the DNS-rebinding TOCTOU window inherent in a separate preflight check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@DrishnaTrivedi

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread litellm/llms/custom_httpx/aiohttp_handler.py
@DrishnaTrivedi

Copy link
Copy Markdown
Contributor Author

@greptileai


for i in range(max(max_retry_on_unprocessable_entity_error, 1)):
try:
response = await async_client_session.post(

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: Redirects can bypass the SSRF guard

ClientSession.post() follows redirects by default, but _assert_not_private_url(api_base) only checks the original URL. An attacker who controls api_base can point it at a public host that returns a redirect to http://169.254.169.254/... or another private IP; validate each redirect target before following it, or disable redirects here and in the sync HTTPHandler path that uses follow_redirects=True.

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