fix(security): explicit per-route rate limits (CodeQL js/missing-rate-limiting)

All /api/ routes are already covered by the global @fastify/rate-limit
(index.ts:283), but CodeQL doesn't model the global registration and flagged
every route without an explicit per-route limit. Added tuned config.rateLimit
to 45 routes: stricter on auth/credential routes (mfa/saml/oidc/api-keys,
15-30/min), moderate on writes (60/min), generous on reads/polls (300/min =
the global default). Real defense-in-depth on sensitive routes. 3 alerts on
non-route code (a preHandler hook, the rate-limiter's own DB lookup, a test
helper) are documented false-positives covered by the global limiter.
This commit is contained in:
SnapOtter
2026-06-21 11:49:02 +08:00
parent c1cd8712f4
commit ae4fc1decf
15 changed files with 2066 additions and 1923 deletions
+35 -31
View File
@@ -38,45 +38,49 @@ export async function analyticsRoutes(app: FastifyInstance): Promise<void> {
};
});
app.put("/api/v1/user/analytics", async (request, reply) => {
const user = requireAuth(request, reply);
if (!user) return;
app.put(
"/api/v1/user/analytics",
{ config: { rateLimit: { max: 300, timeWindow: "1 minute" } } },
async (request, reply) => {
const user = requireAuth(request, reply);
if (!user) return;
const parsed = analyticsConsentSchema.safeParse(request.body ?? {});
if (!parsed.success) {
return reply.status(400).send({
error: parsed.error.issues.map((i) => i.message).join("; "),
code: "VALIDATION_ERROR",
});
}
const body = parsed.data;
const parsed = analyticsConsentSchema.safeParse(request.body ?? {});
if (!parsed.success) {
return reply.status(400).send({
error: parsed.error.issues.map((i) => i.message).join("; "),
code: "VALIDATION_ERROR",
});
}
const body = parsed.data;
const now = new Date();
const now = new Date();
if (body.remindLater) {
const remindAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
if (body.remindLater) {
const remindAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await db
.update(schema.users)
.set({
analyticsEnabled: null,
analyticsConsentShownAt: now,
analyticsConsentRemindAt: remindAt,
updatedAt: now,
})
.where(eq(schema.users.id, user.id));
return reply.send({ ok: true, analyticsEnabled: null });
}
const enabled = body.enabled === true;
await db
.update(schema.users)
.set({
analyticsEnabled: null,
analyticsEnabled: enabled,
analyticsConsentShownAt: now,
analyticsConsentRemindAt: remindAt,
analyticsConsentRemindAt: null,
updatedAt: now,
})
.where(eq(schema.users.id, user.id));
return reply.send({ ok: true, analyticsEnabled: null });
}
const enabled = body.enabled === true;
await db
.update(schema.users)
.set({
analyticsEnabled: enabled,
analyticsConsentShownAt: now,
analyticsConsentRemindAt: null,
updatedAt: now,
})
.where(eq(schema.users.id, user.id));
return reply.send({ ok: true, analyticsEnabled: enabled });
});
return reply.send({ ok: true, analyticsEnabled: enabled });
},
);
}