- management.ts: replace === with timingSafeEqual for CLI token comparison - machineToken.ts: salt upgraded to omniroute-cli-auth-v1; OMNIROUTE_CLI_SALT env var honoured for rotation; full 64-char SHA-256 hex token - tray.ps1: accept .png via GDI+ Bitmap->Icon handle; Windows tray works without .ico - tray.ts: getIconPath() tries icon.ico then icon.png on Windows - compression/types.ts: DEFAULT_CAVEMAN_CONFIG.preservePatterns filled with six defaults (fenced code, inline code, URLs, paths, error lines, stack traces) - CLAUDE.md: Hard Rule #15 — spawn-capable routes must use isLocalOnlyPath() - .env.example + docs/reference/ENVIRONMENT.md: document OMNIROUTE_CLI_SALT - docs/security/CLI_TOKEN.md: new (was referenced in changelog but missing) - docs/security/ROUTE_GUARD_TIERS.md: new (was referenced in changelog but missing) - tests/unit/lib/machineToken.test.ts: updated for 64-char token; added OMNIROUTE_CLI_SALT env-var rotation test
3.3 KiB
Route Guard Tiers
Overview
All OmniRoute management API routes are classified into one of three protection
tiers. Classification is static, defined in src/server/authz/routeGuard.ts,
and evaluated unconditionally on every request before any auth logic runs.
Tiers
Tier 1 — LOCAL_ONLY
Enforced by: isLocalOnlyPath(path) → loopback host check
Bypass: None — not overridable by JWT, CLI token, or requireLogin=false
These routes spawn child processes or execute runtime code. Exposing them to non-loopback traffic would allow an attacker who obtained a valid JWT (e.g., via a Cloudflared/Ngrok tunnel) to trigger process spawning — a known CVE class (GHSA-fhh6-4qxv-rpqj).
| Prefix | Reason |
|---|---|
/api/mcp/ |
MCP server — spawns stdio bridges and SSE handlers |
/api/cli-tools/runtime/ |
CLI tool runtime — executes plugin code |
Response on violation: 403 LOCAL_ONLY
Tier 2 — ALWAYS_PROTECTED
Enforced by: isAlwaysProtectedPath(path) → skip requireLogin=false bypass
Bypass: None when requireLogin=false; JWT always required
These routes are destructive or irreversible. Allowing them in a "no-password" install would mean anyone on the same LAN could wipe the database or kill the server process.
| Path | Reason |
|---|---|
/api/shutdown |
Terminates the server process |
/api/settings/database |
Database export, import, and wipe |
Response on violation: 401 Authentication required
Tier 3 — MANAGEMENT (default)
All other management routes. Auth required unless requireLogin=false is
configured. CLI tokens can authenticate these routes (loopback + valid HMAC).
Evaluation order
managementPolicy.evaluate(ctx)
1. isLocalOnlyPath(path)?
→ not loopback → reject 403 LOCAL_ONLY
2. isInternalModelSyncRequest(ctx)?
→ allow (system)
3. hasValidCliToken(headers)?
→ allow (cli) [loopback + timingSafeEqual HMAC check]
4. isAlwaysProtectedPath(path) or requireLogin=true?
→ isDashboardSessionAuthenticated?
→ allow (dashboard_session)
→ reject 401/403
5. requireLogin=false?
→ allow (anonymous)
Adding a new spawn-capable route
- Add the path prefix to
LOCAL_ONLY_API_PREFIXESinsrc/server/authz/routeGuard.ts - Add a test in
tests/unit/authz/routeGuard.test.tsasserting thatisLocalOnlyPath()returns true for the new prefix - Never skip this step — see Hard Rule #15 in
CLAUDE.md
Files
| File | Purpose |
|---|---|
src/server/authz/routeGuard.ts |
Constants and helper functions |
src/server/authz/policies/management.ts |
Evaluation logic |
tests/unit/authz/routeGuard.test.ts |
Unit tests |
See also
docs/security/CLI_TOKEN.md— CLI machine-ID tokendocs/architecture/AUTHZ_GUIDE.md— full authorization pipeline