diff --git a/apps/admin-api/src/main.ts b/apps/admin-api/src/main.ts index d9fa1e2b9..15b5140b1 100644 --- a/apps/admin-api/src/main.ts +++ b/apps/admin-api/src/main.ts @@ -62,7 +62,7 @@ if (cloudflareAccessMiddleware) { app.use(httpLogger); app.use(requestContextMiddleware); app.use(cookieParser()); -app.use(express.json({ limit: '1mb' })); +app.use(express.json({ limit: '1mb', inflate: false })); app.use('/auth', authRoutes); app.use('/api', apiRoutes); diff --git a/apps/api/src/app/routes/csp-report.routes.ts b/apps/api/src/app/routes/csp-report.routes.ts index 1fd461490..0ce767956 100644 --- a/apps/api/src/app/routes/csp-report.routes.ts +++ b/apps/api/src/app/routes/csp-report.routes.ts @@ -42,7 +42,7 @@ function toOriginAndPath(value: unknown): string | undefined { routes.post( '/', cspReportRateLimit, - json({ type: ['application/csp-report', 'application/reports+json', 'application/json'], limit: '100kb' }), + json({ type: ['application/csp-report', 'application/reports+json', 'application/json'], limit: '100kb', inflate: false }), (req: express.Request, res: express.Response) => { try { const body = req.body; diff --git a/apps/api/src/app/routes/webhook.routes.ts b/apps/api/src/app/routes/webhook.routes.ts index 45d3be025..be716d650 100644 --- a/apps/api/src/app/routes/webhook.routes.ts +++ b/apps/api/src/app/routes/webhook.routes.ts @@ -13,7 +13,7 @@ const webhookRateLimit = createRateLimit('webhook', { keyGenerator: rateLimitGetKeyGenerator(), }); -routes.use(raw({ type: 'application/json' })); +routes.use(raw({ type: 'application/json', inflate: false })); routes.post('/stripe', webhookRateLimit, billingController.webhook.controllerFn()); routes.post('/mailgun', webhookRateLimit, mailgunWebhookController.webhook.controllerFn()); diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 6f8184574..573f93f99 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -319,10 +319,12 @@ if (ENV.NODE_ENV === 'production' && !ENV.CI && cluster.isPrimary) { // handles the report payload instead of the 20mb global JSON parser, and responds 204. app.use('/api/csp-report', cspReportRoutes); - app.use(raw({ limit: '30mb', type: ['text/csv'] })); - app.use(raw({ limit: '30mb', type: ['application/zip'] })); - app.use(json({ limit: '20mb', type: ['json'] })); - app.use(urlencoded({ extended: true })); + // inflate: false - no client sends compressed request bodies; rejecting Content-Encoding (415) keeps + // untrusted input away from zlib/brotli decoders (e.g. CVE-2026-111497) + app.use(raw({ limit: '30mb', type: ['text/csv'], inflate: false })); + app.use(raw({ limit: '30mb', type: ['application/zip'], inflate: false })); + app.use(json({ limit: '20mb', type: ['json'], inflate: false })); + app.use(urlencoded({ extended: true, inflate: false })); // Must run after sessionMiddleware (which loads req.session) and before any route handler, // so an expired pending-verification session is destroyed before a route can observe it. diff --git a/apps/geo-ip-api/src/main.ts b/apps/geo-ip-api/src/main.ts index 2b22e8747..d7a911e68 100644 --- a/apps/geo-ip-api/src/main.ts +++ b/apps/geo-ip-api/src/main.ts @@ -26,8 +26,8 @@ if (ENV.ENVIRONMENT !== 'development' && (!ENV.GEO_IP_API_USERNAME || !ENV.GEO_I const app = express(); -app.use(json({ limit: '20mb' })); -app.use(urlencoded({ extended: true })); +app.use(json({ limit: '20mb', inflate: false })); +app.use(urlencoded({ extended: true, inflate: false })); app.use(httpLogger); app.use(requestContextMiddleware);