fix: stop fetch_url following redirects past the approval gate - #4506
Conversation
…4490) The desktop engine's fetch_url gated on the approved URL, then let urllib follow any 3xx to any host -- including the engine on loopback -- under that same approval, so an author-controlled redirect could pull another chat's transcript, /settings or /logs into the model's context. Fetch now uses a no-redirect opener that raises on any 3xx, so the honest error reaches the model and the user can approve the new URL via a second card. Adds a test standing up a 302 -> secret-body pair and asserting the secret is not returned. Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
|
@coderabbitai review |
|
/review |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Greptile SummaryThe PR prevents the desktop engine’s
Confidence Score: 5/5The PR appears safe to merge and closes the redirect-based approval bypass without introducing an actionable regression. The changed opener rejects every redirect before requesting its target, returns the resulting HTTP error through the existing tool contract, and the regression test verifies that target content does not reach the tool result.
|
| Filename | Overview |
|---|---|
| src/praisonai-desktop/engine/server.py | Replaces automatic redirect following with a local no-redirect opener while preserving the existing tool result and error contract. |
| src/praisonai-desktop/engine/test_chat_stream.py | Adds an isolated end-to-end regression test demonstrating that content behind a redirect is not fetched. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
M[Model requests fetch_url] --> G{Original URL approved?}
G -- No --> D[Return declined result]
G -- Yes --> F[Fetch approved URL]
F --> R{HTTP redirect?}
R -- No --> B[Return sanitized response body]
R -- Yes --> E[Return redirect-not-approved error]
E --> N[Model may request target URL separately]
N --> G
Reviews (1): Last reviewed commit: "fix: stop fetch_url following redirects ..." | Re-trigger Greptile
|
@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Review changes in this PR. Python SDK: praisonaiagents, praisonai. TypeScript SDK: src/praisonai-ts/. Do NOT modify src/praisonai-rust. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task —— View job Final Architecture Review — PR #4506
Verdict: ✅ Approve — correct, minimal, complete. No code changes needed.This is a genuine defensive-security fix that closes a real SSRF / approval-bypass hole. Phase 1 — Architecture (per AGENTS.md)
Phase 2 — Independent verification of the fix
No valid issues were found by me or by prior reviewers (Greptile 5/5; Qodo paused; CodeRabbit no findings) that require a fix. Nothing to skip. Files modified this passNone. The PR as-is is correct and complete; adding anything would be scope creep. --- · Branch: |
|
Merge gate scan — eligible for assessment. Claude merge gate will assess and may auto-merge if |
|
Merged by Claude PR merge gate ( |
Fixes #4490
Problem
The desktop engine's
fetch_url(src/praisonai-desktop/engine/server.py) gated on the approvedurl, then calledurllib.request.urlopen(url)which follows any 3xx redirect to any host — including the engine itself on loopback. A page whose author (or an echoed prior tool result) controls a redirect could send the fetch to another chat's transcript,/settings, or/logs, and the body landed back in the model's context. No re-gate, no scheme check after the first hop — the approval card was a lie about what actually happened.Fix
fetch_urlnow builds an opener with aHTTPRedirectHandlersubclass that raisesHTTPErroron any redirect instead of following it. The honest error reaches the model, which can then ask the user for the new URL and get a second approval card. Minimal, no new params or dependencies.Test
Added
FetchUrlDoesNotFollowRedirectsinengine/test_chat_stream.py: stands up two loopbackThreadingHTTPServers — A returns302 Location: <B>, B returnsSECRET-BODY— setsapproval_mode: "never"so the gate allows, calls thefetch_urlclosure from_builtin_tools()on A's URL, and asserts the secret is not in the result. Asserts the effect (what came back), not that a check was called.All 16 tests in
engine/test_chat_stream.pypass.Generated with Claude Code