security: fix CodeQL vulnerabilities (SSRF, XSS, rate limiting)

- Add URL validation to prevent SSRF attacks on notification endpoints
  - Block private IPs (10.x, 172.16-31.x, 192.168.x, 169.254.x)
  - Block localhost and internal hostnames
  - Only allow HTTP/HTTPS protocols
- Add HTML escaping for medication names in email templates (XSS)
- Add stricter rate limiting for auth routes (5 req/15min for login/register)
- Add SSRF protection tests (405 tests total)
This commit is contained in:
Daniel Volz
2025-12-30 11:52:00 +01:00
parent b5e12c7a95
commit cb1810586d
6 changed files with 138 additions and 9 deletions
+35 -4
View File
@@ -20,6 +20,28 @@ const ARGON2_OPTIONS: argon2.Options = {
hashLength: 32, // 256-bit hash
};
// =============================================================================
// Rate Limiting Configuration for Auth Routes
// =============================================================================
// Stricter rate limits for authentication endpoints to prevent brute-force attacks
const authRateLimitConfig = {
max: 10, // 10 requests
timeWindow: "1 minute", // per minute
errorResponseBuilder: () => ({
error: "Too many requests. Please try again later.",
code: "RATE_LIMIT_EXCEEDED",
}),
};
const sensitiveRateLimitConfig = {
max: 5, // 5 requests
timeWindow: "15 minutes", // per 15 minutes (for login/register)
errorResponseBuilder: () => ({
error: "Too many attempts. Please try again later.",
code: "RATE_LIMIT_EXCEEDED",
}),
};
// =============================================================================
// Validation Schemas
// =============================================================================
@@ -65,7 +87,9 @@ export async function authRoutes(app: FastifyInstance) {
// ---------------------------------------------------------------------------
// POST /auth/register - User registration
// ---------------------------------------------------------------------------
app.post<{ Body: z.infer<typeof registerSchema> }>("/auth/register", async (request, reply) => {
app.post<{ Body: z.infer<typeof registerSchema> }>("/auth/register", {
config: { rateLimit: sensitiveRateLimitConfig },
}, async (request, reply) => {
// Check auth state
const state = await getAuthState();
@@ -123,7 +147,9 @@ export async function authRoutes(app: FastifyInstance) {
// ---------------------------------------------------------------------------
// POST /auth/login - User login
// ---------------------------------------------------------------------------
app.post<{ Body: z.infer<typeof loginSchema> }>("/auth/login", async (request, reply) => {
app.post<{ Body: z.infer<typeof loginSchema> }>("/auth/login", {
config: { rateLimit: sensitiveRateLimitConfig },
}, async (request, reply) => {
const state = await getAuthState();
if (!state.authEnabled) {
@@ -223,7 +249,9 @@ export async function authRoutes(app: FastifyInstance) {
// ---------------------------------------------------------------------------
// POST /auth/refresh - Refresh access token
// ---------------------------------------------------------------------------
app.post("/auth/refresh", async (request, reply) => {
app.post("/auth/refresh", {
config: { rateLimit: authRateLimitConfig },
}, async (request, reply) => {
const refreshTokenCookie = request.cookies.refresh_token;
if (!refreshTokenCookie) {
return reply.status(401).send({ error: "No refresh token", code: "NO_REFRESH_TOKEN" });
@@ -340,7 +368,10 @@ export async function authRoutes(app: FastifyInstance) {
// ---------------------------------------------------------------------------
// PUT /auth/me - Update current user profile
// ---------------------------------------------------------------------------
app.put<{ Body: z.infer<typeof updateProfileSchema> }>("/auth/me", { preHandler: requireAuth }, async (request, reply) => {
app.put<{ Body: z.infer<typeof updateProfileSchema> }>("/auth/me", {
preHandler: requireAuth,
config: { rateLimit: authRateLimitConfig },
}, async (request, reply) => {
const authUser = request.user as unknown as AuthUser | null;
if (!authUser) {
return reply.status(401).send({ error: "Not authenticated" });