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:<any port>` 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 11:54:42 +02:00
parent 33489585a0
commit 27eb301086
+10 -2
View File
@@ -11,7 +11,7 @@ import {feedbackRouter} from './models/feedback/Feedback.router.js';
import {ticketsRouter} from './models/tickets/Tickets.router.js'; import {ticketsRouter} from './models/tickets/Tickets.router.js';
import {adminRouter} from './models/admin/Admin.router.js'; import {adminRouter} from './models/admin/Admin.router.js';
import {auth} from './models/admin/admin.auth.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(); dotenv.config();
@@ -47,7 +47,15 @@ export const createApp = (): express.Application => {
// staging host does not need a code change here. // staging host does not need a code change here.
...ADMIN_ALLOWED_ORIGINS ...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+$/; const localhostRegex = /^http:\/\/localhost:\d+$/;
// Matches http://<private-LAN-IPv4>:<port> - needed so the feedback form can // Matches http://<private-LAN-IPv4>:<port> - needed so the feedback form can
// be reached from a real phone over WiFi during dev (the phone's Origin is // be reached from a real phone over WiFi during dev (the phone's Origin is