Skip to content

Don't follow HTTP redirects on LLM API requests - #10281

Open
dpage wants to merge 2 commits into
pgadmin-org:masterfrom
dpage:llm-no-redirect
Open

Don't follow HTTP redirects on LLM API requests#10281
dpage wants to merge 2 commits into
pgadmin-org:masterfrom
dpage:llm-no-redirect

Conversation

@dpage

@dpage dpage commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

ALLOWED_LLM_API_URLS is checked against the URL pgAdmin has been configured with, but the providers then issued the request through urllib.request.urlopen(), whose default handler will happily follow a Location header on to a destination that check was never applied to. Validating the first URL does not imply validating every subsequent hop.

No legitimate LLM API replies with a redirect, so this refuses them outright rather than re-validating each hop. urlopen_no_redirect() in web/pgadmin/llm/utils.py mirrors urlopen(), including its handling of an explicit SSL context, but installs an HTTPRedirectHandler subclass that raises HTTPError instead of following. All eleven outbound call sites now go through it: two each in the Anthropic, OpenAI and Docker providers, three in Ollama (including the is_available() probe), and the four model-refresh helpers in web/pgadmin/llm/__init__.py. Every one of those sites already caught HTTPError or URLError, so a redirect surfaces as an ordinary provider error and no call site needed restructuring.

I should be clear that this is hardening rather than a fix for an exploitable flaw, because returning the redirect in the first place requires control of a host that is already on the allowlist: either one of the vendor entries, which would mean the vendor itself had been compromised, or a loopback entry, which needs code execution on the pgAdmin host and therefore grants direct access to the same services anyway. The shipped configuration already documents that the loopback entries let any logged-in user reach any port on the server's own loopback interface. The residual case worth closing is an administrator who allowlists an internal self-hosted endpoint that happens to carry an open redirect, and in any event there is no reason for us to be following redirects here at all.

Tests are in web/pgadmin/llm/tests/test_no_redirect.py. They stand up a real loopback HTTP server rather than mocking, and cover 301, 302, 303, 307 and 308 across both GET and POST, asserting each time that the redirect target recorded no hit. There is also a control case asserting that a plain urllib.request.urlopen does follow the same fixture, so the refusal tests cannot pass vacuously against a fixture that never redirected.

Reported by Ziya Abdullayev, whose report prompted the change.

Summary by CodeRabbit

  • Security

    • LLM provider requests now block HTTP redirects, helping prevent requests from being sent to unintended destinations.
    • Redirects are rejected consistently for standard and streaming requests.
  • Bug Fixes

    • Preserved existing request timeouts, SSL settings, response parsing, and error handling.
    • Added coverage for redirect responses and normal successful requests.

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: be1edb0a-7995-4e87-9091-51a925bc64b4

📥 Commits

Reviewing files that changed from the base of the PR and between 235a9fc and b4ee6d4.

📒 Files selected for processing (1)
  • web/pgadmin/llm/utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/pgadmin/llm/utils.py

Included review availability: Your plan includes up to 8 reviews per rolling hour; 6 remain after this review.


Walkthrough

The change adds a shared opener that blocks HTTP redirects for LLM requests. Provider calls and model discovery use the opener. Tests cover redirect rejection, normal responses, and default redirect behavior.

Changes

LLM redirect safety

Layer / File(s) Summary
No-redirect opener and tests
web/pgadmin/llm/utils.py, web/pgadmin/llm/tests/test_no_redirect.py
The utility raises HTTPError for redirects and supports optional SSL contexts. Tests cover GET and POST requests for status codes 301, 302, 303, 307, and 308.
Provider request integration
web/pgadmin/llm/providers/*.py
Anthropic, Docker, Ollama, and OpenAI standard and streaming requests use urlopen_no_redirect. Existing request arguments and response handling remain unchanged.
Model discovery integration
web/pgadmin/llm/__init__.py
Anthropic, OpenAI, Ollama, and Docker Model Runner discovery requests use urlopen_no_redirect.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to b4ee6

The change stops LLM API requests from following redirects and preserves ordinary provider error handling; no actionable merge-blocking risk remains, subject to normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing HTTP redirects for outbound LLM API requests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

dpage added 2 commits August 17, 2026 15:53
ALLOWED_LLM_API_URLS is checked against the URL pgAdmin is configured
with, but the providers then issued the request through
urllib.request.urlopen(), whose default handler would follow a Location
header on to a destination that check was never applied to.

No legitimate LLM API replies with a redirect, so refuse them outright
rather than re-validating each hop: urlopen_no_redirect() installs a
handler that raises HTTPError instead, and the outbound calls in all
four providers and in the model-refresh endpoints now go through it.

This is hardening rather than a fix for an exploitable flaw, since
returning the redirect in the first place requires control of a host
that is already on the allowlist. Reported by Ziya Abdullayev.
HTTPRedirectHandler only gained http_error_308() in Python 3.11, so on
the 3.9 and 3.10 interpreters we still support (and which CI runs) a 308
was never recognised as a redirect: it bypassed _NoRedirectHandler's
redirect_request() entirely and surfaced from http_error_default() as a
bare 'HTTP Error 308: Permanent Redirect'. The redirect was still not
followed, so this was never a security gap, but the caller got no
explanation of why the request had failed, and the test asserting our
message failed on every CI job as a result.

Mapping http_error_308 onto http_error_301, exactly as 3.11 itself does,
routes 308 back through redirect_request() so that every redirect status
behaves identically on every version we support.
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