From 27eb301086a584e9b29b3e42d3423f3a4f4d7229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20M=C3=BCller?= Date: Sun, 6 Sep 2026 11:54:42 +0200 Subject: [PATCH] Fail CORS closed when NODE_ENV is unset The CORS origin check used `NODE_ENV !== 'production'` to decide whether to allow loopback and private-LAN origins. That is not the same question as "is this a dev machine" when NODE_ENV is unset - which is exactly what a fresh Plesk vhost gives you. On such a host the check called it dev and answered `Access-Control-Allow-Origin: http://localhost:` together with `Access-Control-Allow-Credentials: true`. With the admin session cookie scoped to .nachklang.art, that let any page served from localhost on a signed-in admin's machine read their data cross-origin. admin.config.ts already resolves this correctly - only an explicit 'development' or 'test' relaxes anything, so unset is treated as production - so the CORS check now derives from its `isProd` rather than re-deriving its own answer from NODE_ENV. Two places asking the same question two different ways was the bug. Verified against the built app with a vhost-like environment (no NODE_ENV): before, Origin http://localhost:9999 came back allowed with credentials; after, it is rejected, while https://tickets.nachklang.art and https://admin.nachklang.art are still allowed with credentials. With NODE_ENV=development localhost is still allowed, so local development and the LAN exception for real-device testing are unaffected. Co-Authored-By: Claude Opus 5 --- src/app.factory.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app.factory.ts b/src/app.factory.ts index 28a3855..7acca08 100644 --- a/src/app.factory.ts +++ b/src/app.factory.ts @@ -11,7 +11,7 @@ import {feedbackRouter} from './models/feedback/Feedback.router.js'; import {ticketsRouter} from './models/tickets/Tickets.router.js'; import {adminRouter} from './models/admin/Admin.router.js'; import {auth} from './models/admin/admin.auth.js'; -import {ADMIN_ALLOWED_ORIGINS} from './models/admin/admin.config.js'; +import {ADMIN_ALLOWED_ORIGINS, isProd} from './models/admin/admin.config.js'; dotenv.config(); @@ -47,7 +47,15 @@ export const createApp = (): express.Application => { // staging host does not need a code change here. ...ADMIN_ALLOWED_ORIGINS ]; - const isDev = process.env.NODE_ENV !== 'production'; + // `isProd` from admin.config, NOT `NODE_ENV !== 'production'`. The two are not + // the same when NODE_ENV is unset, which is exactly what a fresh Plesk vhost + // gives you: the old test called that "dev" and opened the loopback and + // private-LAN exceptions below. With `credentials: true` on this CORS config + // and a session cookie scoped to .nachklang.art, that let any page served + // from localhost read a signed-in admin's data cross-origin. admin.config + // treats anything but an explicit 'development'/'test' as production, so an + // unset value now fails closed. + const isDev = !isProd; const localhostRegex = /^http:\/\/localhost:\d+$/; // Matches http://: - needed so the feedback form can // be reached from a real phone over WiFi during dev (the phone's Origin is