fix(mcode-webui): round 8 — CORS tightening + cross-origin token-leak fix - #6
Merged
Conversation
Upstream 96ff886 (8/22) is 7 commits behind MiniMax-AI/MiniMax-Code-Plugins#23 head 091dec5 (8/27). This sync brings the plugin distribution tree to parity with PR #23 so all Round 8+ governance work has an upstream home. == Pulled-in commits (in chronological order) == Wzdhehe (9): 33a5861 Add plugin: mcode-webui (initial v1.0.0 release) f81e1e1 docs: rename product name mcode -> Mcode in plugin tree docs d4a886e fix: revert SKILL.md frontmatter name to mcode-webui; strip CRLF 7d21634 fix: revert path references to mcode-webui cae2a80 chore: update GitHub URLs after Mcode-webui rename 5ade9c7 feat(mcode-webui): v1.0.1 - token auth gate + LAN sub-card + read-only mode f2b1c8f fix(mcode-webui): CORS preflight exemption + cross-origin Authorization ffacf58 fix(mcode-webui): round 3 review fixes (auth.js setters + startup smoke test) afd4167 fix(mcode-webui): round 4 - db.js better-sqlite3 path resolver modacker (3): 0dbdacd fix(mcode-webui): round 5 - fix better-sqlite3 resolver + error priority c448424 fix(mcode-webui): round 6 - add standard install candidate 091dec5 feat(mcode-webui): Token Plan key integration end-to-end == File scope (37 files) == 8 added: .gitignore, server/lib/auth.js, test/lib-auth.test.js, test/lib-db-resolver.test.js, test/router-cors.test.js, test/router-readonly.test.js, test/server-startup.test.js, test/usage.test.js 29 modified: server/{router,lib/db,lib/settings,lib/state-bus,lib/usage,lib/config, routes/settings,routes/state,js}, test/{_setup, state-bus,lib-settings,routes-settings}, public/{app/{events,i18n,render,state},index.html, styles/main.css}, docs/{API,ARCHITECTURE,CAPABILITIES, DEVELOPMENT,TROUBLESHOOTING}, package.json, references/SECURITY-NOTES.md, CONTRIBUTING.md, README.md, README.zh-CN.md, server.js == Statistics == 37 files changed, +5195 / -281 == Known gaps (out of scope for this commit) == - Upstream root server/public/test/ stays at 96ff886. The plugin tree is independently maintained; root is for self-hosting. Future sync would require applying Round 5/6/7 deltas to root server/lib/db.js etc. - PR #23 (MiniMax-AI) blocker from hetaoBackend 9/1 (CSRF token disclosure in server/router.js:280 Access-Control-Allow-Origin: *) is NOT addressed here. Planned as Round 8 on a follow-up branch. == Why one commit (not 12) == modacker/MiniMax-Code-Plugins and Wzdhehe/Mcode-webui have no common ancestor (merge-base fails). Cherry-pick is impossible. Wholesale tree copy with attribution in commit body is the honest equivalent. Co-authored-by: Wzdhehe <Wzdhehe@users.noreply.github.com> Co-authored-by: modacker <2918882+modacker@users.noreply.github.com>
… fix hetaoBackend reported on 2026-09-01 against PR #23 head 091dec5 that a malicious page at https://evil.example could exfiltrate the operator's bootstrap auth token from a webui instance running on the same machine via a cross-origin fetch('/api/settings'). Three combined properties created the leak: 1. router.js:279 Access-Control-Allow-Origin: * (any cross-origin page could read responses — no CORS preflight needed for simple GET/POST with JSON body) 2. auth.js:113 isRequestAuthorized() bypassed Gate 3 for isLocalRequest — the request arrived over loopback (127.0.0.1), so no token was required regardless of where the request originated in the browser 3. routes/settings.js + state-bus.js the bootstrap token was included in GET /api/settings (when !tokenAcknowledged), the POST resetToken response, and the auth.token_rotated SSE event payload This commit closes the leak by: * Removing Access-Control-Allow-Origin: * (router.js: setCorsHeaders helper). New behavior: same-origin OR in MCODE_WEBUI_ALLOWED_ORIGINS env allowlist → echo Origin + Vary; otherwise omit CORS headers (browsers block the cross-origin read). OPTIONS preflight still short-circuits before any gate (regression guard). * Making isRequestAuthorized require a valid token for cross-origin requests even when they arrive over loopback (auth.js: new isCrossOriginRequest helper). Same-origin / no-Origin (server-to- server, curl, mcode acp subprocess) keeps the isLocalRequest fast path. * Removing currentToken from every HTTP response and SSE state payload (settings.js getSettingsSnapshot, routes/settings.js resetToken response, state-bus.js pushStateFor + ensureMcodeSessionsFetchedAndPush + pushOnlineCount, plus broadcastTokenRotated which now sends JSON {rotated:true, at:ms} instead of the raw new token). * SPA changes: state.js now clears HEADERS.Authorization + localStorage on auth.token_rotated (no auto-update of HEADERS) and dispatches a window CustomEvent 'webui:token_rotated'. render.js listens for it and shows an 8s toast telling the operator to read the new value from server stdout or ~/.mcode-webui/settings.json. The LAN-card 'token display' placeholder now points to the out-of-band delivery channels. i18n.js adds lan_card_token_saved_v2 (zh+en) and token_rotated_toast (zh+en). UX trade-off: a small one-click convenience (auto-HEADERS-update on rotation) is gone. The operator must re-open the URL with the new ?token=... value. See SECURITY-NOTES §10 for the full contract. Test plan: * Existing csrf-token-disclosure.test.js (added in 091dec5 with 4 blockers) — 3 of 4 RED pre-commit, all 4 GREEN post-commit. The 4th (blocker#4: cross-origin DELETE /api/sessions/:id) was already GREEN pre-commit (404 fallback) and remains GREEN — the underlying Gate 3 tightening now blocks the real cross-origin DELETE too. * router-cors.test.js L279 "Allow-Origin remains wildcard" replaced with a NEGATIVE assertion (source MUST NOT contain '*') and a setCorsHeaders + MCODE_WEBUI_ALLOWED_ORIGINS presence test. L280 + L281 unchanged. * routes-settings.test.js resetToken:true test updated to assert the response does NOT include currentToken and that a 'hint' field points to stdout / settings.json. * state-bus.test.js new describe 'state-bus — currentToken removal (round 8)': per-cid push + __broadcast__ push + broadcastTokenRotated all verified to NOT carry the token value. * BASELINE-2026-09-04.md captures the pre-fix test counts and PoC output for the reviewer to diff against. Drive-by: eslint.config.mjs was at the upstream/ root and imported @eslint/js + globals, but the devDeps live in plugins/Wzdhehe/mcode- webui/package.json — npm install in the subdir didn't satisfy the upstream-root resolve. Moved the config into the mcode-webui subdir so npm run lint works (0 errors, 9 pre-existing 'imported but never used' warnings in test/ that are not new in this commit). Post-commit status: * npm test → 417 / 415 / 0 / 2 (no regressions, +3 new tests) * npm run lint → 0 errors, 9 warnings (none new in this commit) * poc-csrf.mjs → exit 0, 'PoC did not reproduce the blocker' * Same-origin SPA requests (curl with Origin: http://127.0.0.1:PORT) still 200 + ACAO echo + currentToken: '' (field kept for back-compat)
11 tasks
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the cross-origin bootstrap-token-disclosure blocker reported by hetaoBackend on 2026-09-01 against PR #23 head
091dec5. Pre-fix, a malicious page athttps://evil.examplecould exfiltrate the operator's auth token from a webui instance running on the same machine viafetch('http://127.0.0.1:PORT/api/settings'). Three combined server properties created the leak; this PR closes all three.Threat model (3 properties pre-fix)
Access-Control-Allow-Origin: *— any cross-origin page could read responses (no CORS preflight needed for simple GET/POST with JSON body)isRequestAuthorized(req)bypassed Gate 3 forisLocalRequest— cross-origin requests over loopback (127.0.0.1) were treated as local and skipped the token checkGET /api/settingsreturned the bootstrap token in the JSON body whentokenAcknowledged === false(the standard "first run" / "after rotation" state)What this PR changes
GET /api/settingscurrentTokenif!tokenAcknowledged""(field kept for back-compat)POST /api/settings {resetToken: true}currentToken: <new-value>hintpoints to stdout + settings.jsoncurrentTokenif!tokenAcknowledged""auth.token_rotatedSSE event payloadJSON.stringify({rotated:true, at:<ms>})Access-Control-Allow-Origin*MCODE_WEBUI_ALLOWED_ORIGINSenv allowlist; otherwise omitted (browser blocks the cross-origin read)isRequestAuthorizedfor cross-origin over loopbackUX trade-off
Pre-fix, a token rotation auto-updated the operator's
localStorage+ liveHEADERSvia the SSE event payload. The operator did not need to do anything.Post-fix, the operator must:
/api/settingsor the UI button)settings.jsprints it on rotation) OR from~/.mcode-webui/settings.json?token=<new-value>appendedThe SPA shows an 8-second toast on the
auth.token_rotatedevent telling the user exactly what to do. The current tab'sHEADERSare cleared by the SPA, so the next request gets 401 and the browser re-prompts for credentials.Test results
PoC verification
Independent PoC at
poc-csrf.mjs(the script used to report the blocker on 2026-09-01):Files changed
Drive-by
eslint.config.mjswas at the upstream/ root and imported@eslint/js+globals, but the devDeps live inplugins/Wzdhehe/mcode-webui/package.json—npm installin the subdir didn't satisfy the upstream-root resolve. Moved the config into the mcode-webui subdir sonpm run lintworks (0 errors).Reviewer notes
currentTokenfield is kept in the response shape (always"") for back-compat. Existing SPA field references won't break.router-cors.test.jsGate 3 exemption test).Origin: http://127.0.0.1:PORTis echoed back, server-to-server / curl (noOriginheader) doesn't get CORS headers at all (browsers don't enforce CORS in these cases).poc-csrf.mjsagainst the pre-fix code; not reproducible post-fix.References
BASELINE-2026-09-04.md— pre-fix test/PoC snapshotreferences/SECURITY-NOTES.md§10 — full contract and threat modeltest/csrf-token-disclosure.test.js— the regression guard (3 of 4 blockers were RED pre-fix, all 4 GREEN post-fix)