Skip to content

fix(security): require sign-in on seven ungated routers, and admin where they change shared state or reach outside (#16375) - #16418

Merged
mrveiss merged 4 commits into
mainfrom
issue-16375
Sep 13, 2026
Merged

mrveiss merged 4 commits into
mainfrom
issue-16375

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Refs #16375

Single-issue rationale: this gates seven of #16375's routers, in one change of one kind: a router-level sign-in dependency plus per-route admin. api.long_running_operations goes in the next PR, because it needs its own extraction to get under its size ceiling, and its websocket needs its own gate. api.chat_knowledge waits for #16345, which edits that file.

Thinking Path

What an anonymous caller could reach (before this PR)

Router Route(s) What it reached
api.ide_integration (/api/ide) PUT /config Changed the one engine every client on that worker shares. It could switch off the SQL-injection, hardcoded-secret and eval/exec rules for everyone (update_configuration).
POST /analyze, /quickfix, /hover, /batch-analyze, /completion; GET /rules, /categories, /severities In-memory regex and AST analysis of submitted code, plus Redis cache writes (the completion and context caches). No subprocess, file or outbound access: the "shell/exec" hits are regex strings in the lint rules.
api.knowledge_crawl POST /api/knowledge/crawl BFS-crawled caller-chosen seeds. KB writes are on by default (ingest=True → store_fact, Redis and ChromaDB). An unguarded robots.txt GET went to every seed and linked host (SSRF, #16378). A scrape_template_id reached the headless template runner, whose own router needs admin.
api.knowledge_scrape POST /api/knowledge/scrape A guarded fetch of any public URL, returned to the caller: an open fetch proxy. ingest=true wrote to the autobot_docs ChromaDB collection.
api.knowledge_site_map POST /api/knowledge/site-map An unguarded GET of <caller scheme://host:port/path>/sitemap.xml, plus every child <loc> of a sitemap index, with <loc> values reflected back. That's SSRF to internal hosts (#16378).
api.knowledge_extract POST /api/knowledge/extract A guarded fetch, then an LLM call with the caller's JSON Schema (LLM spend), then an optional write to autobot_docs.
api.run_jwt_router POST /api/runs/{run_id}/jwt/refresh Not usable without a valid run JWT: the handler checks the signature, audience, denylist and run_id binding. But a refreshed token has no absolute lifetime (#16379).
api.web_research_settings (/api/web-research) PUT /settings Persistent config writes to settings.json, including require_user_confirmation and the rate limits.
POST /test Drove the headless research browser with the caller's query, using up the shared rate limit.
POST /reset-circuit-breakers, /clear-cache Switched off the breaker's protection, and cleared the cache.
POST /enable, /disable Always 500 today on a broken import (#16380), so nothing changed.
GET /status, /settings, /usage-stats Status, config and stats, with no secrets.

What Changed

  • Router-level Depends(get_current_user) on all seven routers, so a route added later is gated by default.

  • Per-route Depends(check_admin_permission) on every route that changes shared config or drives the research browser:

    Level Routes
    Admin (7) PUT /api/ide/config; POST /api/web-research/enable, /disable, /test, /clear-cache, /reset-circuit-breakers; PUT /api/web-research/settings
    Authenticated (16) the other eight /api/ide/* routes; POST /api/knowledge/crawl, /scrape, /site-map, /extract; POST /api/runs/{run_id}/jwt/refresh; GET /api/web-research/status, /settings, /usage-stats

    Owner decision: the web-research routes (crawl/scrape/site-map/extract) are for any signed-in user — each still reaches outside on a caller-chosen URL, but only the web-research settings mutations (enable, disable, PUT /settings, test, clear-cache, reset-circuit-breakers) stay admin.

  • run-JWT refresh stays authenticated-only. get_current_user accepts a run-scoped JWT (SEC-2 feat(security): run-scoped short-lived JWTs to limit blast radius of leaked agent credentials #6473), so a run refreshing its own token still passes. The handler still binds the token to run_id, and revokes it.

  • Moves needed to edit at all:

  • Room under the size ceiling: PATTERN_RULES (184 lines) moves unchanged to api/ide_pattern_rules.py, and api.ide_integration re-imports it.

    • ide_integration.py goes 1110 → 928 lines, and its ceiling is lowered in both repo_tests/python_file_size_ratchet_baseline.py and scripts/python_file_size_known_large.py.
    • The moved list was checked AST-identical to the original.
  • Changelog: changelog/unreleased/16375-router-auth.md.

Behaviour change, stated plainly:

  • Non-admin users keep the Scrape, Crawl, Site map and Extract tabs. (composables/knowledge/useWebResearch.ts:107-119.) Any signed-in user can reach these four routes, per the owner's decision above.
  • Non-admin users lose the web-research toggles. (stores/useWebResearchStore.ts: enable/disable, settings, test, clear-cache, reset-circuit-breakers.) A non-admin now gets 403 there.
  • No SLM-frontend or service-to-service callers of these paths were found.

Verification

  • CI: pending.
  • autobot-backend/tests/api/test_router_auth_16375.py tests with the real dependencies, on fix(security): authenticate every skills route, and require admin to change, fetch or execute (#16368) #16370's pattern. Under pytest auth_middleware is the conftest stub, which grants admin, so the test overrides exactly the objects each router bound with the real functions from real_auth_middleware.
    • Every route is classified: the pinned policy table equals the seven routers' 23 routes.
    • Anonymous callers get 401 on all 23.
    • A non-admin gets 403 on all 7 admin routes.
    • A non-admin gets 200 on the four research POSTs (crawl, scrape, site-map, extract — outbound I/O stubbed, so only the auth gate is under test) and on GET /api/ide/categories (the pre-existing control).
  • Existing tests that import these routers (tests/api/test_web_research_settings_guard.py, tests/unit/api/test_knowledge_crawl.py, api/knowledge_scrape_test.py, api/web_research_status_timeout_12751_test.py) run under the admin-granting stub. The new dependencies resolve to it there, as with fix(security): authenticate every skills route, and require admin to change, fetch or execute (#16368) #16370.
  • Local checks: pre-commit and pre-push hooks only. I executed no code from the repo.

Sequencing: #16370's router-auth guard adds a baseline of the ungated routers, with a stale-entry check. Once it merges, base gets merged here and these seven routers' entries get removed.

Filed while tracing: #16378 (SSRF in the robots.txt and sitemap fetches, which auth doesn't close), #16379 (unbounded run-JWT refresh), #16380 (web-research enable/disable always 500), #16381 (IDE completion cache key and unbounded input).

Model Used

Claude Opus 5 (claude-opus-5)

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: d59bbe4c-d580-4f89-9e35-d85574cefe3f


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.

@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

There is no PR queue limit, and this is not a request to defer this PR. Work proceeds one issue at a time without a cap on open PRs; review capacity is the constraint.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@mrveiss

mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

CI red at c651fb43f: this PR's own new completeness test, cause not fully isolated. test_router_auth_16375.py::test_the_policy_table_covers_every_route asserts _routes(_app()) == set(_POLICY); the actual side (_routes(_app())) comes back completely empty, so all 15 _POLICY entries read as missing.

_app() builds a bare FastAPI() and calls include_router(module.router, prefix=...) for each of 7 directly-imported modules (import api.ide_integration as ide_api, etc. — not going through the app factory or router registry). I ruled out autobot-backend/conftest.py stubbing api.* broadly (it doesn't, unlike the SLM conftest's AST-stubbing that caused a similar-shaped empty-result bug on #16236) — so that's not the cause here.

I could not pin the exact reason _app() collects zero routes with the budget I had. Worth checking next: whether these 7 router modules define their routes via a decorator that needs the app's own lifespan/startup to run first (so a bare FastAPI() never sees them), or whether module.router is being shadowed by something these modules do at import time in this test's collection order specifically.

@mrveiss mrveiss added this to the v0.9.0 milestone Sep 12, 2026
@mrveiss mrveiss self-assigned this Sep 12, 2026
@mrveiss

mrveiss commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Security review at 92b29ef0d: APPROVE WITH NITS. Refs #16375 is correct: seven of the issue's routers are gated here, not all of them.

Verified at this head:

  • Each of the seven modules gates at router level with APIRouter(dependencies=[Depends(get_current_user)]):
    • ide_integration.py:82
    • knowledge_crawl.py:54
    • knowledge_extract.py:61
    • knowledge_scrape.py:59
    • knowledge_site_map.py:46
    • run_jwt_router.py:26
    • web_research_settings.py:39
  • Admin checks sit on the mutating routes: check_admin_permission appears twice in ide_integration.py and 7 times in web_research_settings.py.
  • No overlap. security(auth): admin-gate the open routers, settings, teams and user-account routes (#15745, #16278, #16276, #16277, #15738, #16279) #16240's f5fe68888 gated developer, frontend_config, redis, settings and settings_config. Open security(routers): gate knowledge_search, knowledge_search_aggregator, knowledge_suggestions and voice_stream (#16507) #16508 gates knowledge_search, knowledge_search_aggregator, knowledge_suggestions and voice_stream. Neither list shares a module with this PR, so nothing is double-gated and there is no conflicting dependency.
  • The 07:40Z red at c651fb43f is fixed. 92b29ef0d reads routes per router through effective_routes, because walking the mounted app on FastAPI ≥0.139 returned nothing. At this head the latest checks show nothing red and nothing pending.
  • tests/api/test_router_auth_16375.py uses the real_auth_middleware fixture, not the stubbed admin grant.
  • From the review pass, not re-derived by me:
    • The tests assert 401 for anonymous callers on all 23 routes and 403 for non-admins on the 7 admin routes.
    • The policy table covers every route in the seven modules.
    • None of them has a websocket route.
Severity Where Note
Nit tests/api/test_router_auth_16375.py It mounts the routers on a bare FastAPI(), not through the real registry. So a prefix or dependency drift in feature_routers.py would not be caught here. That is acceptable under the FastAPI ≥0.139 constraint, and #16370's config-router guard covers the registry side once it lands.
Info repo_tests/python_file_size_ratchet_baseline.py, scripts/python_file_size_known_large.py Open #16508 also edits both files. The review pass read the lines as non-adjacent, but whichever PR merges second should re-check the counts.

…e they change shared state or reach outside (#16375)

api.ide_integration, api.knowledge_crawl, api.knowledge_scrape, api.knowledge_site_map,
api.run_jwt_router and api.web_research_settings were mounted with no auth dependency.
Each router now depends on get_current_user; PUT /ide/config, the three knowledge
POSTs and six web-research routes also need check_admin_permission. run_jwt stays
authenticated-only (get_current_user accepts run-scoped JWTs).

To make the edits possible: the knowledge and run-jwt models move to
schemas_knowledge_web.py / schemas_run_jwt.py (no-local-schemas hook), and
PATTERN_RULES moves to api/ide_pattern_rules.py so ide_integration.py drops from
its 1110-line ceiling to 928 (ceiling lowered). Tests use the real dependencies.
…in only where it reaches outside (#16375)

api.knowledge_extract was mounted with no auth dependency at all -- not
even the router-level sign-in #16375's first pass gave the other six.
It fetches a caller-chosen URL, spends an LLM call on a caller-supplied
JSON Schema, and can write the knowledge base, so it gets the same
Depends(get_current_user) at router level as the other six, plus
Depends(check_admin_permission) on its one route.

ExtractRequest/ExtractResponse move unchanged to
api/schemas_knowledge_web.py, joining the other three knowledge routes'
models there: the no-local-schemas hook (#6056) blocks any edit of a
router file that still defines its own BaseModel.

test_router_auth_16375.py's policy table and mount list grow to cover
this seventh router.
test_the_policy_table_covers_every_route asserted set() against 23
expected routes: under fastapi>=0.139, app.routes holds one opaque
_IncludedRouter wrapper per include_router call and no APIRoute, so
reading it directly finds nothing (#15093's shape, already hit and
fixed the same way on #16370).

_routes() now walks each of the seven routers individually via
autobot_shared.api_routing.router_routes.effective_routes and prepends
the already-known static prefix from _MOUNTS, rather than trying to
recover the include-time prefix from the framework -- matching
test_skills_auth_16368.py's fix for the identical failure.
…6375)

Batch 1 brought repo_tests/config_router_auth_coverage_test.py onto
main, and 03's batch-2a build failed
test_the_known_ungated_list_has_not_gone_stale on this branch: this PR
adds Depends(get_current_user) to api.ide_integration,
api.knowledge_crawl, api.knowledge_scrape, api.knowledge_site_map,
api.run_jwt_router and api.web_research_settings, but all six were
still listed in KNOWN_UNGATED, which only ever shrinks.

Verified directly against enumerate_config_routers() rather than
trusting the router diff alone: all six now resolve gated=True.
api.knowledge_extract (also touched by this PR) was never in
KNOWN_UNGATED to begin with, so nothing to remove there -- confirmed
gated=True as well, for completeness.

Test logic (KNOWN_UNGATED, UNREADABLE, the two assertions) untouched;
only the six now-gated entries are removed from the frozenset.
@mrveiss

mrveiss commented Sep 13, 2026

Copy link
Copy Markdown
Owner Author

Delta 92b29ef0d..2ebcd96b0 (the rebase onto main plus the list fix): approve. The change is list removals only.

  • The only file whose change differs from the approved net diff is repo_tests/config_router_auth_coverage_test.py. Its net diff removes exactly six KNOWN_UNGATED entries: api.ide_integration, api.knowledge_crawl, api.knowledge_scrape, api.knowledge_site_map, api.run_jwt_router and api.web_research_settings. Each of the six is on main's list. Nothing is added.
  • api.knowledge_extract was never on main's KNOWN_UNGATED, so there was nothing to remove for the seventh module.
  • The six router modules, their tests and the ratchet files are unchanged from the approved version; the other commits are the rebased originals.
  • This fixes the batch-2a red on test_the_known_ungated_list_has_not_gone_stale, which landed after my approval.

The branch is 0 behind main, and CI has 23 checks pending with none red.

@mrveiss
mrveiss merged commit 9179f93 into main Sep 13, 2026
78 checks passed
@mrveiss
mrveiss deleted the issue-16375 branch September 13, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant