temporarily allow any origin on /api/music

Off unless ALLOW_ANY_ORIGIN_MUSIC=true, so it is opt-in per host and reverting is an env
edit rather than a deploy. Set on this host now; the flag and the two call sites come out
when the tailnet becomes the perimeter and the music app stops needing to be public.

It drops one layer, not the lock: /api/music still requires a valid token through
userMiddleware, and the non-owner account backstop in originScopeMiddleware still confines
music accounts to /api/auth + /api/music whatever Origin they claim. Worth being plain
about what is lost — Origin was never authentication here. `officer://<hex>` is chosen by
the client, trivially forged outside a browser, and extractable from any shipped app
binary. It is defence in depth, and this removes it for one path prefix.

Two gates had to know: userMiddleware, which is what actually 403s, and the CORS origin
callback, which would otherwise echo an empty origin and block a browser client that
userMiddleware had already allowed through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 10:49:14 +00:00
co-authored by Claude Opus 5
parent 1e6b01cd21
commit 418729a470
3 changed files with 23 additions and 3 deletions
@@ -85,6 +85,18 @@ for (const { slug, origin } of APP_ORIGIN_LIST) {
// Last, so the web origin wins if an app ever declares the same one.
if (PUBLIC_ORIGIN) ORIGIN_RULES[PUBLIC_ORIGIN] = { superAdminOnly: true };
// ── TEMPORARY: origin exemption for /api/music ──
// Off unless ALLOW_ANY_ORIGIN_MUSIC=true. While the platform is still internet-exposed, this lets a music
// client that cannot present the app's custom-scheme origin reach /api/music anyway. It drops ONE layer:
// a valid token is still required (userMiddleware) and the non-owner account backstop still confines
// music accounts. Origin was never authentication — it is client-chosen and trivially forged outside a
// browser — so this removes defence in depth, not the lock. Delete once the tailnet is the perimeter.
const ALLOW_ANY_ORIGIN_MUSIC = process.env.ALLOW_ANY_ORIGIN_MUSIC === 'true';
export function isMusicOriginExempt(path: string): boolean {
return ALLOW_ANY_ORIGIN_MUSIC && pathAllowed(path, ['/api/music']);
}
// True when an Origin is reserved for the platform owner (used at signin to reject a non-owner login).
export function isSuperAdminOnlyOrigin(origin: string | undefined): boolean {
return !!origin && ORIGIN_RULES[origin]?.superAdminOnly === true;
+7 -3
View File
@@ -1,7 +1,7 @@
import type { MiddlewareHandler } from 'hono';
import { verify } from '@@/jwt';
import * as errors from '@@/custom-errors';
import { isOriginAllowed } from './origin-validation';
import { isOriginAllowed, isMusicOriginExempt } from './origin-validation';
import { isLockdown, noteBlocked } from '../api/auth/panic';
import { getUserById, isTokenBlacklisted } from 'officerdb';
@@ -24,10 +24,14 @@ export const userMiddleware: MiddlewareHandler = async function (ctx, next) {
}
if (!token) throw errors.UNAUTHORIZED();
// Validate origin for authenticated routes
// Validate origin for authenticated routes.
// TEMPORARY, opt-in: ALLOW_ANY_ORIGIN_MUSIC=true drops the Origin check for /api/music only, so a
// client that can't present the app's custom-scheme origin can still reach the music API. Auth is
// untouched — a valid token is still required, and the non-owner account backstop in
// originScopeMiddleware still applies. Delete this and the env var once the tailnet is the perimeter.
const origin = ctx.get('origin') as string | undefined;
const host = ctx.req.header('host');
if (!isOriginAllowed(origin, host)) {
if (!isMusicOriginExempt(ctx.req.path) && !isOriginAllowed(origin, host)) {
throw errors.FORBIDDEN('Invalid origin');
}
+4
View File
@@ -43,6 +43,7 @@ import { pipelineJobsRouter } from './api/tasks/pipeline-jobs-routes';
import { broadcastPanelRefresh } from './api/terminal/websocket';
import { CustomError } from './custom-errors';
import { userMiddleware, bodyParser, isOriginAllowed, originScopeMiddleware } from './_middlewares';
import { isMusicOriginExempt } from './_middlewares/origin-validation';
export { Hono };
export { createRouter };
@@ -54,6 +55,9 @@ honoServer.use(
cors({
origin: (origin, c) => {
const host = c.req.header('host');
// TEMPORARY: see isMusicOriginExempt — echoes any Origin back for /api/music when enabled, so a
// browser client is not blocked by CORS after userMiddleware has already let it through.
if (isMusicOriginExempt(c.req.path)) return origin ?? '*';
return isOriginAllowed(origin, host) ? origin : '';
},
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],