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
+9 -3
View File
@@ -5,7 +5,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { env } from "../config.js";
import { db, schema } from "../db/index.js";
import { auditLog } from "../lib/audit.js";
import { auditLog, sanitizeAuditInput } from "../lib/audit.js";
import { getPermissions, requirePermission } from "../permissions.js";
const scryptAsync = promisify(scrypt);
@@ -221,13 +221,19 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
.get();
if (!user || !user.passwordHash) {
auditLog(request.log, "LOGIN_FAILED", { username: body.username, reason: "unknown_user" });
auditLog(request.log, "LOGIN_FAILED", {
username: sanitizeAuditInput(body.username),
reason: "unknown_user",
});
return reply.status(401).send({ error: "Invalid credentials" });
}
const valid = await verifyPassword(body.password, user.passwordHash);
if (!valid) {
auditLog(request.log, "LOGIN_FAILED", { username: body.username, reason: "bad_password" });
auditLog(request.log, "LOGIN_FAILED", {
username: sanitizeAuditInput(body.username),
reason: "bad_password",
});
return reply.status(401).send({ error: "Invalid credentials" });
}