🛡️ Sentinel: Add rate limiting to /api/og endpoint (#47)

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-03-27 10:36:25 +01:00
committed by GitHub
parent c8f75e4615
commit 43d5b4d15d
2 changed files with 15 additions and 10 deletions

View File

@@ -1,10 +1,7 @@
## 2024-03-12 - [Missing Rate Limiting on EXIF Endpoint] ## 2024-05-18 - Unprotected Dynamic Image Generation Endpoints
**Vulnerability:** The `/api/exif/[id]` endpoint lacked rate limiting, making it vulnerable to DoS attacks. An attacker could rapidly request EXIF metadata, exhausting server resources and the Immich API rate limits. **Vulnerability:** The `/api/og` route generates an `ImageResponse` dynamically using `next/og`. Because this operation is computationally expensive and memory-intensive, unauthenticated endpoints lacking rate limiting act as prominent Denial of Service (DoS) vectors. Attackers can flood the endpoint with varying parameters, forcing the server to continually spawn compute-heavy tasks until resources are exhausted and the instance crashes or latency spikes to unacceptable levels.
**Learning:** Even simple GET endpoints that fetch metadata from an upstream server (like Immich) must be protected with rate limiting to prevent downstream DoS and resource exhaustion, especially when they perform backend API requests per hit.
**Prevention:** Always implement rate limiting on endpoints that proxy requests or query a backend API, utilizing the shared `checkRateLimit` utility.
## 2024-03-20 - [DoS via Upstream Healthcheck Abuse] **Learning:** Next.js dynamic endpoints that do not hit external upstream APIs or databases (like image generation with `next/og`) are often overlooked for rate limiting. Rate limiting is just as critical for protecting local compute resources as it is for protecting downstream API quotas or databases. Any route performing on-the-fly heavy processing must implement throttling.
**Vulnerability:** The `/api/health` endpoint lacked downstream protection and made a live upstream request to the Immich server (`immich.ping()`) on every hit. An attacker could exhaust backend resources by spamming the health check endpoint.
**Learning:** Returning HTTP 429 (rate limiting) on a health check endpoint is an architectural anti-pattern that breaks load balancers and container orchestrators (causing self-inflicted downtime). **Prevention:** Apply the shared `checkRateLimit` utility (from `@/lib/rate-limit`) to all computationally expensive endpoints (such as `next/og` usage), even if they do not explicitly query external services. Ensure `getConfig().rateLimitRpm` is passed as the threshold to maintain configurable global protection.
**Prevention:** To secure health checks that rely on upstream resources without breaking infrastructure polling, implement a short-lived in-memory cache (e.g., 10 seconds) for the upstream response rather than blocking the request entirely.

View File

@@ -7,14 +7,22 @@
*/ */
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { getConfig } from '@/lib/config'; import { getConfig } from '@/lib/config';
import { checkRateLimit, getClientIp } from '@/lib/rate-limit';
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const ip = getClientIp(request);
const { theme, rateLimitRpm } = getConfig();
const { success } = checkRateLimit(`og:${ip}`, rateLimitRpm);
if (!success) {
return new NextResponse('Too many requests', { status: 429 });
}
const { searchParams } = request.nextUrl; const { searchParams } = request.nextUrl;
const title = (searchParams.get('title') || 'Gallery').slice(0, 200); const title = (searchParams.get('title') || 'Gallery').slice(0, 200);
const subtitle = (searchParams.get('subtitle') || '').slice(0, 100); const subtitle = (searchParams.get('subtitle') || '').slice(0, 100);
const { theme } = getConfig();
return new ImageResponse( return new ImageResponse(
<div <div