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:
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user