fix(security): close three middleware gaps around the API edge

Stable port of the main commit; the admin-preview and maintenance-gate
items do not exist on this branch.

- the general rate limiter skipped anyone holding any verified JWT; a
  gallery token is minted for free on password-less galleries and slideshow
  links, so that was an unlimited budget for every /api route. Only admin
  sessions skip now
- the 50mb JSON limit is scoped to /api/admin and /api/v1; everything else
  gets 2mb, so an unauthenticated body can no longer stall JSON.parse
- the CSRF Content-Type gate accepted multipart from any origin; cross-site
  form posts are now rejected via Sec-Fetch-Site / Origin, with a Host match
  fallback for same-origin installs that leave FRONTEND_URL unset

(cherry picked from commit 839bf4e4, adapted)
This commit is contained in:
Paul Nothaft
2026-09-03 12:14:02 +02:00
parent a8d57f0d69
commit b1369068ae
4 changed files with 137 additions and 20 deletions
+7 -2
View File
@@ -106,8 +106,13 @@ function isAuthenticated(req) {
return false;
}
// Valid token found - check type
req.tokenType = decoded.type; // 'admin' or 'gallery'
// Only an admin session earns the skip. A gallery token is minted for
// free on password-less galleries and slideshow links, so treating it as
// "authenticated" handed anyone an unlimited budget on every /api route.
if (decoded.type !== 'admin') {
return false;
}
req.tokenType = decoded.type;
req.tokenPayload = decoded;
return true;
+40
View File
@@ -0,0 +1,40 @@
/**
* Origin allow-listing shared by the CORS options and the multipart CSRF gate
* in server.js. Kept apart from server.js so it can be unit-tested without
* booting the app.
*/
function isAllowedOrigin(origin) {
const allowedOrigins = [
process.env.FRONTEND_URL || 'http://localhost:3005',
process.env.ADMIN_URL || 'http://localhost:3005'
];
if (process.env.NODE_ENV === 'development') {
allowedOrigins.push(
'http://localhost:5173', // Vite dev server
'http://localhost:3002', // Backend server
'http://localhost:3001', // For API testing
'http://localhost:3000' // Direct backend access
);
}
return allowedOrigins.indexOf(origin) !== -1;
}
// Origin check for multipart bodies (see the Content-Type gate below).
// Same-origin installs proxy /api through nginx and may not have FRONTEND_URL
// set, so an Origin matching the request Host is accepted alongside the CORS
// allowlist; Sec-Fetch-Site is authoritative when a browser sends it.
function multipartOriginAllowed(req) {
const site = req.headers['sec-fetch-site'];
if (site) return site !== 'cross-site';
const origin = req.headers.origin;
if (!origin) return true;
if (isAllowedOrigin(origin)) return true;
try {
return new URL(origin).host === req.headers.host;
} catch {
return false;
}
}
module.exports = { isAllowedOrigin, multipartOriginAllowed };