fix(security): harden API against pentest findings

- Default TRUST_PROXY=false to prevent XFF rate limit bypass (PT-01)
- Return 400 instead of 500 on malformed JSON input (PT-03)
- Default MAX_PIPELINE_STEPS=20 to prevent DoS (PT-04)
- Validate clientJobId length (max 128) across all routes (PT-06)
- Add security headers to all reply.hijack() streaming responses (PT-07)
- Sanitize usernames in audit log to prevent stored XSS (PT-08)
- Block TRACE method with 405 response (PT-10)
- Add 429 RateLimited response to OpenAPI spec (PT-12)
- Default MAX_SVG_SIZE_MB=50 to limit SVGZ decompression (PT-13)
- Pin Dockerfile base images by digest
- Sanitize OIDC IdP error and sub claim in audit log
- Sync Docker compose/Dockerfile defaults with env.ts
This commit is contained in:
SnapOtter
2026-06-07 21:54:27 +08:00
parent 19f40f58c9
commit ace41168bc
20 changed files with 122 additions and 24 deletions
+2
View File
@@ -3,6 +3,7 @@ import { extname } from "node:path";
import archiver from "archiver";
import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { getSecurityHeaders } from "../../lib/csp.js";
import { formatZodErrors } from "../../lib/errors.js";
import { sanitizeFilename } from "../../lib/filename.js";
@@ -72,6 +73,7 @@ export function registerBulkRename(app: FastifyInstance) {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="renamed-${jobId.slice(0, 8)}.zip"`,
"Transfer-Encoding": "chunked",
...getSecurityHeaders(),
});
const archive = archiver("zip", { zlib: { level: 5 } });
+2
View File
@@ -4,6 +4,7 @@ import type { FastifyInstance } from "fastify";
import sharp from "sharp";
import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js";
import { getSecurityHeaders } from "../../lib/csp.js";
import { formatZodErrors } from "../../lib/errors.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { sanitizeFilename } from "../../lib/filename.js";
@@ -146,6 +147,7 @@ export function registerFavicon(app: FastifyInstance) {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="favicons-${jobId.slice(0, 8)}.zip"`,
"Transfer-Encoding": "chunked",
...getSecurityHeaders(),
});
const archive = archiver("zip", { zlib: { level: 5 } });
+4 -1
View File
@@ -57,7 +57,10 @@ export function registerOcr(app: FastifyInstance) {
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
clientJobId = part.value as string;
const raw = part.value as string;
if (typeof raw === "string" && raw.length > 0 && raw.length <= 128) {
clientJobId = raw;
}
}
}
} catch (err) {
+4 -1
View File
@@ -157,7 +157,10 @@ export function registerPassportPhoto(app: FastifyInstance) {
fileBuffer = Buffer.concat(chunks);
filename = sanitizeFilename(part.filename ?? "image");
} else if (part.fieldname === "clientJobId") {
clientJobId = part.value as string;
const raw = part.value as string;
if (typeof raw === "string" && raw.length > 0 && raw.length <= 128) {
clientJobId = raw;
}
}
}
} catch (err) {
+2
View File
@@ -5,6 +5,7 @@ import type { FastifyInstance } from "fastify";
import sharp from "sharp";
import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js";
import { getSecurityHeaders } from "../../lib/csp.js";
import { formatZodErrors } from "../../lib/errors.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { sanitizeFilename } from "../../lib/filename.js";
@@ -162,6 +163,7 @@ export function registerSplit(app: FastifyInstance) {
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="split-${jobId.slice(0, 8)}.zip"`,
"Transfer-Encoding": "chunked",
...getSecurityHeaders(),
});
const archive = archiver("zip", { zlib: { level: 5 } });
+6 -1
View File
@@ -7,6 +7,7 @@ import PQueue from "p-queue";
import sharp from "sharp";
import { z } from "zod";
import { env } from "../../config.js";
import { getSecurityHeaders } from "../../lib/csp.js";
import { resolveConcurrency } from "../../lib/env.js";
import { formatZodErrors } from "../../lib/errors.js";
import { sanitizeFilename } from "../../lib/filename.js";
@@ -129,7 +130,10 @@ export function registerSvgToRaster(app: FastifyInstance) {
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
} else if (part.fieldname === "clientJobId") {
clientJobId = part.value as string;
const raw = part.value as string;
if (typeof raw === "string" && raw.length > 0 && raw.length <= 128) {
clientJobId = raw;
}
}
}
} catch (err) {
@@ -321,6 +325,7 @@ export function registerSvgToRaster(app: FastifyInstance) {
"Transfer-Encoding": "chunked",
"X-Job-Id": jobId,
"X-File-Results": encodeURIComponent(JSON.stringify(fileResultsMap)),
...getSecurityHeaders(),
});
const archive = archiver("zip", { zlib: { level: 5 } });