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:
+20
-18
@@ -206,25 +206,13 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
// CORS configuration (apply only to API routes)
|
||||
const { isAllowedOrigin, multipartOriginAllowed } = require('./src/utils/requestOrigin');
|
||||
|
||||
const corsOptions = {
|
||||
origin: function (origin, callback) {
|
||||
const allowedOrigins = [
|
||||
process.env.FRONTEND_URL || 'http://localhost:3005',
|
||||
process.env.ADMIN_URL || 'http://localhost:3005'
|
||||
];
|
||||
|
||||
// In development, also allow localhost origins
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
// Allowlist lives in utils/requestOrigin, shared with the multipart gate.
|
||||
// Allow requests with no origin (like curl) and allow-listed origins
|
||||
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
|
||||
if (!origin || isAllowedOrigin(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
// Do not error globally; just omit CORS headers on disallowed origins
|
||||
@@ -452,8 +440,14 @@ async function initializeRateLimiters() {
|
||||
}
|
||||
|
||||
// Note: Rate limiters will be initialized after database connection
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
// Body limits. 50mb is only needed by the authenticated admin and API-token
|
||||
// surfaces (restore manifests, CMS and email templates, bulk operations);
|
||||
// applied globally it let any unauthenticated caller hand JSON.parse a 50mb
|
||||
// body and block the event loop. express.json skips a request whose body
|
||||
// is already parsed, so the scoped parser must run first.
|
||||
app.use(['/api/admin', '/api/v1'], express.json({ limit: '50mb' }));
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '2mb' }));
|
||||
|
||||
// CSRF protection: require JSON Content-Type on mutating API requests
|
||||
// This blocks cross-origin form submissions which cannot set Content-Type: application/json
|
||||
@@ -465,6 +459,14 @@ app.use('/api', (req, res, next) => {
|
||||
if (contentLength > 0 && !contentType.includes('application/json') && !contentType.includes('multipart/form-data')) {
|
||||
return res.status(415).json({ error: 'Unsupported Content-Type. Use application/json or multipart/form-data.' });
|
||||
}
|
||||
// multipart is exactly what a cross-site <form> can send without a
|
||||
// preflight, and in a split-origin deployment (SameSite=None) the admin
|
||||
// cookie rides along to the upload routes. Browsers label such a
|
||||
// submission Sec-Fetch-Site: cross-site (and always send Origin on a
|
||||
// cross-origin POST); non-browser clients send neither header and pass.
|
||||
if (contentType.includes('multipart/form-data') && !multipartOriginAllowed(req)) {
|
||||
return res.status(403).json({ error: 'Cross-site multipart request rejected' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user