fix(security): authenticate the /terminal edge route (unauth RCE)#113
Open
mt-alarcon wants to merge 1 commit into
Open
fix(security): authenticate the /terminal edge route (unauth RCE)#113mt-alarcon wants to merge 1 commit into
mt-alarcon wants to merge 1 commit into
Conversation
…0.0.1 bind CRITICAL finding: Traefik router `evonexus_terminal` (priority 10) exposed the terminal-server (port 32352) directly on /terminal/* with only stripprefix middleware — bypassing Flask login entirely, allowing unauthenticated RCE via `claude --dangerously-skip-permissions`. Three defence-in-depth layers: 1. Traefik forwardauth — adds `evonexus_terminal_auth` middleware that probes Flask `/api/auth/check` before forwarding any /terminal request. Flask validates the session cookie and returns 200 (allow) or 401 (deny). New endpoint in auth_routes.py; added to PUBLIC_PATHS so auth_middleware doesn't pre-reject it before the session is resolved. [UPSTREAM-PR] 2. WS token (shared secret) — server.js reads TERMINAL_WS_TOKEN env var and enforces it via WebSocket.Server verifyClient hook (HTTP 401 on upgrade rejection). terminal_proxy.py appends ?token=<value> when connecting upstream so the legitimate Flask-proxied path keeps working. Dev mode (token unset) logs a warning but stays open for local development. [UPSTREAM-PR] 3. Bind on 127.0.0.1 — terminal-server no longer binds on 0.0.0.0 by default. TERMINAL_BIND_HOST env var overrides (dev convenience). [UPSTREAM-PR] Both stack files updated: evonexus.stack.yml (upstream template) and [C]evonexus-contabo.stack.yml (MTA prod deployment — the actually deployed stack). Both add forwardauth middleware and TERMINAL_WS_TOKEN env var. [CONFIG] for the contabo stack; [UPSTREAM-PR] for the template. Tests: 6 Node tests (node --test) — 4 new WS auth tests + 2 pre-existing. All PASS. Python syntax validated. YAML validated. WAF Cloudflare rule blocking /terminal* must remain until Marcello deploys this branch and confirms the fix is live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @mt-alarcon, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
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.
Problem
The Traefik router
evonexus_terminal(priority 10) publishes the terminal-server (port 32352) directly on/terminal/*with only astripprefixmiddleware — no authentication. This bypasses the Flask@login_requiredguard entirely (that path never reaches Flask). The terminal-server has no auth of its own, binds0.0.0.0, runscors()wide-open, and spawnsclaude --dangerously-skip-permissions→ unauthenticated RCE at the edge for any self-hosted instance exposing this stack.Fix — defence in depth (3 layers)
evonexus_terminal_authmiddleware probes FlaskGET /api/auth/checkbefore forwarding any/terminalrequest; Flask validates the session cookie and returns 200 (allow) / 401 (deny). New endpoint added toPUBLIC_PATHSso the auth middleware doesn't pre-reject it before the session resolves.server.jsreadsTERMINAL_WS_TOKENand enforces it via averifyClienthook (timingSafeEqual, HTTP 401 on upgrade rejection). The Flask proxy appends?token=upstream, so the legitimate proxied path keeps working and the browser never sees the token. When unset, it logs a warning and stays open (local dev convenience).127.0.0.1by default (override viaTERMINAL_BIND_HOST);cors()restricted toEVONEXUS_DASHBOARD_ORIGINin production.Tests
dashboard/terminal-server/test/server.test.js— 4 new WS-auth tests (accept w/ correct token, reject w/o token → 401, reject wrong token → 401, dev-mode open when unset) + 2 pre-existing.node --test→ 6/6 pass.Deploy note
Set
TERMINAL_WS_TOKEN(e.g.openssl rand -hex 32) in the deploy environment. Theforwardauth.addressuses127.0.0.1:8080, correct for single-node Swarm; multi-node deployments should use the dashboard service name instead.🤖 Generated with Claude Code