Skip to content

fix(dashboard): restrict requireAdminToken query-param to GET only (PILOT-238)#89

Open
matthew-pilot wants to merge 2 commits into
mainfrom
openclaw/pilot-238-20260630-023001
Open

fix(dashboard): restrict requireAdminToken query-param to GET only (PILOT-238)#89
matthew-pilot wants to merge 2 commits into
mainfrom
openclaw/pilot-238-20260630-023001

Conversation

@matthew-pilot

Copy link
Copy Markdown
Collaborator

What failed

The requireAdminToken middleware accepted admin_token=<token> as a query parameter for all HTTP methods — GET, POST, PUT, DELETE, etc. This creates a CSRF vector: if an operator copies a URL containing admin_token=... into chat, browser history, or logs, an attacker can auto-submit a hidden form targeting mutation endpoints like /api/admin/runtime/gc?admin_token=..., /api/admin/runtime/log-level, or /api/admin/whitelist.

The /api/banner handler already had an inline guard (merged in PR #7), but requireAdminToken — used by /api/stats, /api/pulse, /api/badge/*, /api/admin/runtime/*, /api/admin/whitelist, /api/admin/networks/*, /api/admin/backups, and /api/admin/audit/* — was still unconditionally accepting query-param tokens on all methods.

Why this fix

Restrict the query-param fallback to GET requests only. POST/PUT/DELETE must present the admin token via the X-Admin-Token header, which cannot be set by cross-site form submissions (HTML forms cannot send custom headers).

Files changed

  • dashboard/dashboard.go: requireAdminToken now checks r.Method == http.MethodGet before falling back to query-param token (+8/-4)
  • dashboard/zz_stats_auth_test.go: new TestAdminToken_CSRF_MutationRejectsQueryParam that verifies POST with ?admin_token=... returns 401, while the header token continues to work (+37/-0)

Verification

go build ./...    ✅
go vet ./...      ✅
go test ./...     ✅ (all packages pass)

Closes PILOT-238

…on endpoints

TestAdminToken_CSRF_MutationRejectsQueryParam verifies that
POST /api/admin/runtime/gc with ?admin_token=... returns 401,
while X-Admin-Token header continues to work. This is the
repro case for PILOT-238 — the requireAdminToken middleware
accepts query-param tokens on all HTTP methods, creating a
CSRF vector for any password-leak (URL in logs, referer, etc).

The test is written to fail before the fix and pass after.
…ILOT-238)

The requireAdminToken middleware accepted admin_token=<token> as a
query parameter for all HTTP methods — GET, POST, PUT, DELETE, etc.
This creates a CSRF vector: if an operator copies a URL containing
admin_token=... into chat/browser-history/logs, an attacker can
auto-submit a hidden form pointing at that URL and trigger mutations
like /api/admin/runtime/gc?admin_token=..., /api/admin/runtime/log-level,
or /api/admin/whitelist mutations.

Fix: the query-param fallback is restricted to GET requests only.
POST/PUT/DELETE must present the admin token via the X-Admin-Token
header, which cannot be set by cross-site form submissions.

The /api/banner handler already had an inline guard for this (merged
in PR #7), but requireAdminToken — used by /api/stats, /api/pulse,
/api/badge/*, /api/admin/runtime/*, /api/admin/whitelist, /api/admin/
networks/*, and /api/admin/backups — was still unconditionally accepting
query-param tokens on all methods.

Closes PILOT-238
@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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