From ff392e694ccf1ad712746667e04754b2dc9ef3d8 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 27 Jun 2026 13:08:08 -0300 Subject: [PATCH] =?UTF-8?q?fix(api):=20LAN/Tailscale=20dashboard=20?= =?UTF-8?q?=E2=80=94=20host-aware=20CSP=20+=20GET-exempt=20version=20route?= =?UTF-8?q?=20+=20combo=20field=20errors=20(#5083)=20(#5177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.39. Host-aware CSP (ReDoS/injection-safe host validation) + GET-exempt /api/system/version (POST/spawn stays LOCAL_ONLY, exact-match safe-methods-only) + COMBO_002 firstField. 44/44 tests + route-guard membership gate green. CHANGELOG synced to keep all 4 v3.8.39 fixes. --- CHANGELOG.md | 1 + src/app/api/combos/[id]/route.ts | 9 +- src/middleware.ts | 65 +++++++ src/server/authz/policies/management.ts | 2 +- src/server/authz/routeGuard.ts | 41 ++++- src/server/csp.ts | 116 +++++++++++++ tests/unit/api/combo-002-first-field.test.ts | 109 ++++++++++++ .../route-guard-version-get-exemption.test.ts | 97 +++++++++++ tests/unit/csp-host-aware.test.ts | 163 ++++++++++++++++++ 9 files changed, 600 insertions(+), 3 deletions(-) create mode 100644 src/middleware.ts create mode 100644 src/server/csp.ts create mode 100644 tests/unit/api/combo-002-first-field.test.ts create mode 100644 tests/unit/authz/route-guard-version-get-exemption.test.ts create mode 100644 tests/unit/csp-host-aware.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2467307ebf..be0dd0d877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ _In development β€” bullets added per PR; finalized at release._ ### πŸ”§ Bug Fixes +- **fix(api): LAN/Tailscale dashboard access β€” host-aware CSP, GET-exempt version route, surface combo field errors** β€” three failures when opening the dashboard from a non-loopback host: (1) CSP `connect-src` was a static loopback-only string, blocking `ws://:*` WebSocket connections from LAN/Tailscale clients; the CSP is now built per-request from a validated `Host` header (`src/server/csp.ts` + `src/middleware.ts`) with a strict hostname/IPv4 regex so injection-shaped values are never interpolated; (2) `GET /api/system/version` was blocked by `LOCAL_ONLY_API_PREFIXES` for all methods despite only `POST` spawning child processes (git/npm/pm2) β€” a new `LOCAL_ONLY_API_GET_EXEMPTIONS` set exempts safe read methods for this path while keeping `POST`/`PUT`/`PATCH`/`DELETE` strictly loopback-only; (3) `COMBO_002` validation errors only surfaced the generic message β€” `firstField`/`firstMessage` are now extracted from the first Zod issue and included in the response body. ([#5083](https://github.com/diegosouzapw/OmniRoute/issues/5083) β€” thanks @KooshaPari for the diagnosis and original PR #5084) - **fix(sse): defer `` close so it never leaks before `tool_calls` in Claudeβ†’OpenAI streaming** β€” when a Claude thinking block was followed by a tool_use block, the translator unconditionally emitted a `content: ""` chunk at `content_block_stop`, injecting a spurious assistant text chunk immediately before the `tool_calls` delta and corrupting OpenAI-compatible clients (e.g. Kimi Coding). The close marker is now deferred: it is flushed at the first `text_delta` that follows the thinking block (preserving the #4633 / decolua/9router#454 behavior for Claude Code / Cursor) or at stream finish when no tool_calls were collected. Tool-use streams never get a `text_delta` after the thinking block, so `` is never emitted into content before `tool_calls`. ([#5123](https://github.com/diegosouzapw/OmniRoute/issues/5123)) - **fix(sse): normalize array user-message content in the Command Code executor to prevent upstream 400** β€” when a client sends a user turn whose `content` is an array of content parts (e.g. `[{type:"text",text:"…"}, …]`), the raw array was forwarded verbatim to the Command Code upstream, which requires `messages[N].content` for the `user` role to be a plain string β€” resulting in `expected string, received array` / HTTP 400 on DeepSeek V4-Pro and other Command Code models. The user branch of `convertMessages` now calls `normalizeContentText()` (already used by system, assistant, and tool branches) so multi-part user content is joined to a string before dispatch. Partially addresses ([#5166](https://github.com/diegosouzapw/OmniRoute/issues/5166)); the 0-output-token symptom on reasoning-only models is tracked separately. diff --git a/src/app/api/combos/[id]/route.ts b/src/app/api/combos/[id]/route.ts index 46f6280727..4946006266 100644 --- a/src/app/api/combos/[id]/route.ts +++ b/src/app/api/combos/[id]/route.ts @@ -117,10 +117,17 @@ export async function PUT(request, { params }) { const { id } = await params; const validation = validateBody(updateComboSchema, rawBody); if (isValidationFailure(validation)) { + // Surface the first field-level issue so clients can highlight the + // offending field without parsing the full issues array (#5083 Bug 3). + const firstDetail = validation.error.details?.[0] ?? null; return comboErrorResponse( "COMBO_002", 400, - { issues: validation.error }, + { + issues: validation.error, + firstField: firstDetail?.field ?? null, + firstMessage: firstDetail?.message ?? null, + }, request ); } diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000000..21443db57e --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,65 @@ +/** + * Next.js Edge Middleware β€” per-request Content-Security-Policy (#5083). + * + * The static CSP in next.config.mjs only covers loopback origins in + * connect-src. When OmniRoute is reached from a LAN, Tailscale, or public + * hostname the dashboard cannot establish WebSocket connections because + * ws://:* is absent from the static policy. + * + * This middleware reads the trusted Host header, validates it with a strict + * regex, and β€” for valid non-loopback hosts β€” appends + * ws://:* http://:* + * to connect-src before the response reaches the browser. + * + * The CSP header set here overrides the static next.config.mjs header + * because middleware runs before the static route headers are applied. + * The static CSP is kept in next.config.mjs as a build-time fallback for + * environments where middleware is disabled. + * + * Security: + * - Host values are validated with a bounded hostname/IPv4 regex before + * interpolation. Invalid / injection-shaped values are ignored. + * - /dashboard/providers/services/*/embed/* keeps "frame-ancestors 'self'" + * (overrides the baseline "frame-ancestors 'none'") so the embedded + * service UI can be iframed by the OmniRoute dashboard. + * - All other hard-coded security directives (object-src, form-action …) + * are preserved verbatim from the baseline. + */ + +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { buildCspForHost } from "@/server/csp"; + +/** Path prefix for embedded service reverse-proxy pages (Hard Rule #17). */ +const EMBED_PREFIX = "/dashboard/providers/services/"; + +export function middleware(request: NextRequest): NextResponse { + const response = NextResponse.next(); + + const { pathname } = request.nextUrl; + const host = request.headers.get("host"); + + // Embedded service UI pages need `frame-ancestors 'self'` so that the + // OmniRoute dashboard can render them inside an