Skip to content

fix(review): J1 — real bugs from post-refactor review sweep (SSRF, tools:false 500, shutdown NameError, +2)#168

Merged
AVADSA25 merged 1 commit into
mainfrom
review-fixes-1
May 31, 2026
Merged

fix(review): J1 — real bugs from post-refactor review sweep (SSRF, tools:false 500, shutdown NameError, +2)#168
AVADSA25 merged 1 commit into
mainfrom
review-fixes-1

Conversation

@AVADSA25
Copy link
Copy Markdown
Owner

Summary

After the route-extraction series wrapped, I ran a read-only review sweep (code-review + security-review + dead-code agents) over the extracted modules. This PR fixes the 5 real bugs they surfaced. Each is verified against source and pinned by a new test file (tests/test_review_fixes_j1.py, 17 tests).

Findings fixed

# Severity File Bug
1 Security (CWE-918) routes/chat.py SSRF — chat URL auto-fetch had no internal-host guard
2 Bug routes/chat.py UnboundLocalError → opaque 500 on POST /api/chat {"tools": false}
3 Bug (move regression) routes/chat.py _enrich_messages repo_dir one dirname too shallow after the H1 move
4 Bug codec_dashboard.py _shutdown_services NameError on the moved _qchat_conn/_vibe_conn
5 Bug routes/vibe_exec.py /api/run_code ran java/cpp/sql as python instead of rejecting

Detail

1. SSRF guard. _enrich_messages auto-fetches any URL found in chat content — exactly the prompt-injection path. Added _url_host_is_public() (resolves host, rejects loopback / private / link-local incl. 169.254.169.254 metadata / reserved / multicast / non-http) and made _fetch_url_content validate pre-fetch + follow redirects manually (≤5 hops, re-validating each Location) so a public URL can't 302→internal. CODEC is loopback-only by default; this keeps the dashboard_host: 0.0.0.0 opt-in safe. (Residual DNS-rebinding TOCTOU accepted for a local app, noted in-code.)

2. tools:false 500. last_user_text / has_attachment were bound only inside if use_tools:, but _build_chat_system_prompt(...) is called with both regardless. Hoisted before the gate.

3. repo_dir. After H1 moved this code into routes/, dirname(abspath(__file__)) points at routes/, not the repo root where codec_search.py lives. Now climbs two levels (matches web_search.py). Was masked because the dashboard already has repo root on sys.path — but the line's behavior silently changed, violating the verbatim-move goal.

4. shutdown NameError. The handler did global _qchat_conn, _vibe_conn then read them — but those singletons moved to routes/qchat.py + routes/vibe.py in D1/D2, so they were never module names in codec_dashboard → NameError on every shutdown before any close ran. Now closes them in their real modules.

5. unsupported language. ext_map listed java/cpp/sql but cmd_map didn't, so cmd_map.get(lang, [python3.13]) silently ran them as python. Now returns 400 Unsupported language: X. Also removed a dead body.get("filename", ...) bare expression.

Bonus parity: the non-stream post-LLM path now strips a non-allowlisted [SKILL:...] tag from the answer (the streaming path already did). Cosmetic — the execution-gating invariant was intact on both paths already.

Test plan

  • tests/test_review_fixes_j1.py — 17 tests: SSRF block-list (9 params) + public-allow + fetch-returns-empty, hoisted-bindings, two-level repo_dir, shutdown-no-NameError, unsupported-language-400, python-still-accepted
  • python3.13 -m pytest --ignore=tests/test_skills.py -q2,072 passed, 77 skipped
  • ruff check: 0 issues
  • SSRF guard verified: 127.0.0.1/localhost/169.254.169.254/192.168.*/10.*/::1/ftp:/file:/0.0.0.0 all blocked; 1.1.1.1 allowed
  • _shutdown_services() runs clean

Branches off main; independent of the in-flight #167 (different files/regions) — merges cleanly in either order.

🤖 Generated with Claude Code

Five concrete findings from the code-review + security-review pass after the
route-extraction series. Each verified against source + pinned by a new test
(tests/test_review_fixes_j1.py, 17 tests).

1. SSRF guard on chat URL auto-fetch (CWE-918, routes/chat.py)
   _enrich_messages auto-fetches URLs found in chat content — the prompt-
   injection vector. Added _url_host_is_public(): resolves the host and rejects
   loopback / private / link-local (incl. 169.254.169.254 metadata) / reserved
   / multicast / non-http. _fetch_url_content now validates pre-fetch AND
   follows redirects manually (≤5 hops, re-validating each Location) so a
   public URL can't 30x-redirect to an internal one. Keeps the
   dashboard_host:0.0.0.0 opt-in safe.

2. UnboundLocalError on POST /api/chat {"tools": false} (routes/chat.py)
   last_user_text / has_attachment were bound only inside `if use_tools:`, but
   _build_chat_system_prompt(...) is called with both regardless → opaque 500.
   Hoisted both before the gate.

3. _enrich_messages repo_dir was one dirname too shallow (routes/chat.py)
   After the H1 move into routes/, os.path.dirname(abspath(__file__)) resolves
   to routes/, not the repo root where codec_search.py lives. Now climbs two
   levels (matches the web_search.py extraction). Was masked only because the
   dashboard already has repo root on sys.path.

4. _shutdown_services NameError (codec_dashboard.py)
   The handler declared `global _qchat_conn, _vibe_conn` and read them, but
   those singletons moved to routes/qchat.py + routes/vibe.py in D1/D2 — they
   were never module-level names in codec_dashboard anymore, so shutdown raised
   NameError before closing anything. Now closes them in their real modules.

5. /api/run_code ran unsupported languages as python (routes/vibe_exec.py)
   ext_map listed java/cpp/sql but cmd_map didn't, so cmd_map.get(lang, [python3.13])
   silently fed them to python3.13. Now returns 400 "Unsupported language: X".
   Also dropped the dead `body.get("filename", ...)` bare expression.

Bonus parity: the non-stream post-LLM path now strips a non-allowlisted
[SKILL:...] tag from the answer (the streaming path already did) — cosmetic,
the execution-gating invariant was already intact on both paths.

Full suite: 2,072 passed / 77 skipped (+17 new J1 tests). ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AVADSA25 AVADSA25 merged commit f79cc65 into main May 31, 2026
1 check passed
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.

2 participants