Repository navigation
security(api): every route in api/skills.py (catalog fetch/install, enable, execute) is unauthenticated, and the router-auth guards can't see feature routers #16368
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Sep 11, 2026 - added a commit that references this issue
on Sep 13, 2026 Post-merge audit: all 4 acceptance criteria verified on
origin/mainPR #16370, merge commit
afccf73b4. The issue stays closed.AC Evidence on origin/main1. No route in skills.pyorskills_hub.pyserves an unauthenticated request; a test per routeautobot-backend/api/skills.py:58andskills_hub.py:25both useAPIRouter(… dependencies=[Depends(get_current_user)]).autobot-backend/tests/api/test_skills_auth_16368.py:148test_the_policy_table_covers_every_route.:154test_an_anonymous_caller_is_refusedexpects 401 and is parametrized over every route.:164checks a non-admin gets 403, and:171is the control.2. The guard reads every registry, including feature routers, and fails on a planted ungated one repo_tests/config_router_auth.py:25-28CONFIG_REGISTRIEScoversfeature,analytics,integration,monitoringandterminal. The core registry keeps its existing guard.repo_tests/config_router_auth_coverage_test.pytest_a_planted_ungated_feature_router_is_caughtplants the router infeature_routers.py.3. Other ungated routes fixed or in a capped baseline with a reason and an issue config_router_auth_coverage_test.py:25KNOWN_UNGATED. Its reason is in the comment above it: "no gate of any detectable kind… tracked on #16375". The cap is enforced by:104test_no_new_config_router_is_ungated_and_undocumentedand by:113test_the_known_ungated_list_has_not_gone_stale, which only lets the list shrink.4. The PR records what an anonymous executecould reach, from codePR #16370's body, section "What an anonymous caller could reach". It was traced on base a12b4bf79with code links toenable_skill,execute_skillandSkillManager.execute_skill.Reopening: criterion 2 is not met on
main. #16370 (merged asafccf73b4, my PR) delivered criteria 1, 3 and 4. It missed one registry.# Criterion On main 1 No skills / skills_hub route serves an unauthenticated request, with a test per route Met. api/skills.py:58andapi/skills_hub.py:25applydependencies=[Depends(get_current_user)]at router level, withcheck_admin_permissionon execute, install and uninstall.tests/api/test_skills_auth_16368.pyasserts_routes() == set(_POLICY)(:150) and a 401 for every route (:157), through the real auth middleware.2 The router-auth guard reads every router registry, and fails on a planted ungated feature route Not met. The planted self-test works ( config_router_auth_coverage_test.py:146). Butrepo_tests/config_router_auth.pyCONFIG_REGISTRIESreads feature/analytics/integration/monitoring/terminal, and its comment saysmcp_routers.py"lists none of its own: its routers are incore_routers.py". That is false.initialization/router_registry/mcp_routers.py:48,50listsapi.autobot_mcp_routerandapi.mcp_token_admin. Neither appears incore_routers.py, and both are mounted throughload_mcp_routers()(initialization/routers.py).3 Other ungated feature routes are fixed, or baselined in a capped list with a reason and an issue Met. KNOWN_UNGATEDhas 36 entries. New entries fail (:106), and the list can only shrink (:115). They are tracked on #16375, which is open.4 The PR records, from code, what an anonymous executecould reachMet. The "What an anonymous caller could reach" section of #16370 cites skills/manager.pyat base.No route is exposed today.
autobot_mcp_routerchecks its bearer token inside the handler, andmcp_token_adminusesrequire_role("admin", "superadmin"). But an ungated router added tomcp_routers.pywould pass the guard unseen, which is the gap this issue exists to close.The fix: add
"mcp"toCONFIG_REGISTRIES, correct the comment, and baseline or fix whatever the widened guard then reports. This is a new PR, so it waits for the fresh train, per the current merge-all-first rule. The batch-1 vehicle #16577 ran the test suite with #16370 in it: 11 of 12 python-suite shards report SUCCESS, and shard 6's status was not reported.Reconciling the two audit comments above: the reopen is correct, and I've unticked AC2. The earlier post-merge audit counted AC2 as met, but
repo_tests/config_router_auth.py'sCONFIG_REGISTRIESdoesn't includemcp_routers.py. Its comment says those routers are incore_routers.py, which isn't true forapi.autobot_mcp_routerorapi.mcp_token_admin. So the guard doesn't read every registry, and a new ungated MCP router would pass unseen. No route is exposed today (both gate in-handler or throughrequire_role). The fix is to add the MCP registry toCONFIG_REGISTRIESand correct the comment; it goes to the fresh train after the current PRs merge. ACs 1, 3 and 4 stay ticked with the evidence above.github-actions commented
on Sep 13, 2026 on Sep 13, 2026 – with GitHub ActionsContributorMore actionsFourth criterion verified against merged
main— closingThree criteria were already ticked. The fourth was not, and it is met. Evidence read from
origin/main.AC2 — met. "The router-auth guard reads every router registry, including feature routers, and fails on a planted ungated route."
Reads every registry, not just
core_routers.py—repo_tests/config_router_auth.py:25-27:CONFIG_REGISTRIES: tuple[str, ...] = tuple( f"{BACKEND}/initialization/router_registry/{group}_routers.py" for group in ("feature", "analytics", "integration", "monitoring", "terminal") )
Its own module docstring names why that matters and is worth quoting, because it states the exact gap
this issue was filed for: "router_auth_enumerator.REGISTRYreads onlycore_routers.py...api.skills
sat infeature_routers.py, anonymous, for exactly that reason."Fails on a planted ungated route —
config_router_auth_coverage_test.py:129,
test_a_planted_ungated_feature_router_is_caught, which builds a synthetic tree with one feature router
carrying an ungated@router.post("/run")and asserts:assert [(v.module, v.gated) for v in enumerate_config_routers(tmp_path)] == [("api.planted", False)] assert _ungated(tmp_path) == {"api.planted"}
And it has the matching positive control —
test_a_planted_gated_feature_router_passesplants the
same router gated at router level. That pairing is what makes the negative meaningful: without it, a
guard that flagged everything would pass the first test and be useless.The suite also carries
test_the_sweep_reaches_every_config_registered_router,
test_the_known_ungated_list_has_not_gone_staleand
test_the_routers_this_sweep_cannot_read_are_declared— so the enumerator cannot silently narrow, the
tolerance list cannot outlive its reason, and anything unreadable is declared rather than skipped.That last trio is the difference between a guard and a guard that can tell nothing found from did not
look, which is the distinction this criterion was really asking about.All four criteria now hold. Closing.
Verified during the v0.9.0 closure pass.
- added a commit that references this issue
on Sep 17, 2026
Problem
At the application layer, every route in
autobot-backend/api/skills.pyis reachable by an unauthenticated caller. The file contains noDepends(...),get_current_userorcheck_admin_permission. That includes the catalog fetch and install routes, and the skillenableandexecuteroutes.autobot-backend/api/skills_hub.py's/installroutes are ungated too. No repo guard can see it: both router-auth guards read only the core router registry.Found in a read-only audit on 2026-09-11 while verifying a CodeQL dismissal (#16302). Checked on
origin/Dev_new_guiata12b4bf79.The chain, with evidence
include_routeraddsprefix=f"/api{prefix}"andtagsonly, neverdependencies=, for core and feature routers alike.autobot-backend/app_factory.py:83(core),:91(optional/feature)("api.skills", "/skills", ["skills"], "skills")has no auth element.initialization/router_registry/feature_routers.py:579AuthenticationMiddleware(auth_middleware.py:44) is never added withapp.add_middleware; it's only aDependssource.enforce_service_authis installed and enforcing by default, but it acts only onSERVICE_ONLY_PATHS, and/api/skills/*isn't one of them, so it callscall_next.initialization/middleware.py:100;middleware/service_auth_enforcement.py:114-135, 397-438core_router_auth_guard_test.pyimportsload_core_routersonly.repo_tests/router_auth_enumerator.pysetsREGISTRYtocore_routers.py.api.skillsisn't incore_routers.py, so it's structurally invisible to both. It isn't an allowlisted exemption.initialization/router_registry/core_router_auth_guard_test.py:137-142;repo_tests/router_auth_enumerator.py:45;repo_tests/router_auth_coverage_test.py:35-46skills_repos.pygatesPOST /and/{repo_id}/syncwithDepends(check_admin_permission). Every mutating route inskills_governance.pyis admin-gated.api/skills_repos.py:66,125;api/skills_governance.py:84,158,184,263,306Impact (what's known and what isn't)
_validate_catalog_url+pinned_connector, redirects off). But an anonymous caller can make the backend fetch any public URL. There's no rate limit, andSkillInstallRequest.catalog_urlhas nomax_length(api/schemas_agent.py:1691-1695).SkillPackagerow (state=INSTALLED,trust_level=SANDBOXED,external_importer.py:195-211). Promotion is admin-gated, andgovernance.py:163-183blocks auto-promotion of imported skills. Whether a catalog-installed row can be promoted at all is not determined.enableandexecuteinapi/skills.py: these are ungated too. What an anonymousexecutecan reach hasn't been traced yet, meaning which registered skills exist and what they can do. It's the most urgent part of the fix.Fix
api/skills.pyandapi/skills_hub.py, failing closed. Reads need an authenticated user. Install, enable, disable, sync, catalog fetch and execute needcheck_admin_permission, matching the siblings, unless a narrower permission exists for execute. Check the frontend's callers so the UI keeps working, and state the chosen policy per route in the PR.router_auth_enumerator.py'sREGISTRY(and the core guard, or a sibling) must also readfeature_routers.pyand any other registry. Freeze every other currently-ungated feature route found in a named, capped baseline with a reason each, so it can't grow, and file those separately. Add a planted self-test: an ungated feature route must fail.Acceptance criteria
api/skills.pyorapi/skills_hub.pyserves an unauthenticated request. There's a test per route.executecould reach before the fix, as evidence from code.Refs #16302, #16242, #16248