API Defense in Depth: Rate Limiting, Validierung, Header
API-Sicherheitsebenen jenseits der Authentifizierung: Rate Limiting, Validierung, Output-Encoding, Header und Audit-Logging mit TypeScript-Middleware.

Die Authentifizierung verifiziert die Identität. Sie beantwortet die Frage „Wer bist du?“ Aber ein korrekt authentifizierter Benutzer kann deine API trotzdem missbrauchen — durch fehlerhafte Daten, das Überlasten von Endpunkten, das Ausnutzen der Geschäftslogik oder das Extrahieren von Daten über unbegrenzte Abfragen. Die Ebenen jenseits der Authentifizierung entscheiden darüber, ob deine API den Kontakt mit dem echten Traffic übersteht.
Defense in Depth bedeutet, dass jede Ebene davon ausgeht, dass die vorherige versagen könnte. Rate Limiting setzt nicht voraus, dass die Firewall den Angriff blockiert hat. Die Eingabevalidierung setzt nicht voraus, dass der Client wohlgeformte Daten gesendet hat. Output-Encoding setzt nicht voraus, dass die Datenbank saubere Werte enthielt.
Rate Limiting: Kapazität schützen
Rate Limiting verhindert, dass ein einzelner Client unverhältnismäßig viele Ressourcen verbraucht. Ohne es kann ein einziger aggressiver Client — böswillig oder fehlerhaft — den Dienst für alle anderen lahmlegen.
// ❌ No rate limiting — any client can overwhelm the API
app.post("/api/search", async (req, res) => {
const results = await db.fullTextSearch(req.body.query);
res.json(results);
// An attacker sends 10,000 requests/second
// Database overwhelmed, other users get timeouts
});// ✅ Layered rate limiting middleware
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
keyPrefix: string;
}
async function checkRateLimit(
key: string,
config: RateLimitConfig
): Promise<{
allowed: boolean;
remaining: number;
resetAt: number;
}> {
const windowKey =
`${config.keyPrefix}:${key}:` +
`${Math.floor(Date.now() / config.windowMs)}`;
const count = await redis.incr(windowKey);
if (count === 1) {
await redis.pexpire(windowKey, config.windowMs);
}
const remaining = Math.max(
0,
config.maxRequests - count
);
const resetAt =
Math.ceil(Date.now() / config.windowMs) *
config.windowMs;
return {
allowed: count <= config.maxRequests,
remaining,
resetAt,
};
}
function rateLimiter(config: RateLimitConfig) {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
// Use authenticated user ID, fall back to IP
const key =
req.user?.id ?? req.ip ?? "unknown";
const result = await checkRateLimit(key, config);
// Always set rate limit headers
res.set({
"X-RateLimit-Limit": String(config.maxRequests),
"X-RateLimit-Remaining": String(result.remaining),
"X-RateLimit-Reset": String(result.resetAt),
});
if (!result.allowed) {
res.status(429).json({
error: "Too many requests",
retryAfter: Math.ceil(
(result.resetAt - Date.now()) / 1000
),
});
return;
}
next();
};
}
// Different limits for different endpoints
app.use(
"/api/search",
rateLimiter({
windowMs: 60_000,
maxRequests: 30,
keyPrefix: "rl:search",
})
);
app.use(
"/api/auth/login",
rateLimiter({
windowMs: 900_000, // 15 minutes
maxRequests: 5, // Strict for auth
keyPrefix: "rl:login",
})
);Eingabevalidierung: Fehlerhafte Daten früh ablehnen
Jeder Anfrageparameter ist eine nicht vertrauenswürdige Eingabe. Validiere Form, Typ und Einschränkungen vor der Verarbeitung.
import { z } from "zod";
// ❌ Trusting client input
app.post("/api/users", async (req, res) => {
// req.body could be anything — no validation
await db.query(
"INSERT INTO users (name, email, role) VALUES ($1, $2, $3)",
[req.body.name, req.body.email, req.body.role]
// Attacker sets role: "admin" — privilege escalation
);
});// ✅ Strict schema validation with Zod
const createUserSchema = z.object({
name: z
.string()
.min(1)
.max(100)
.regex(
/^[\p{L}\p{N}\s\-'.]+$/u,
"Invalid characters in name"
),
email: z.string().email().max(254),
// Role is NOT user-settable — determined by backend
});
const searchSchema = z.object({
query: z
.string()
.min(1)
.max(200)
.transform((q) => q.trim()),
page: z.coerce
.number()
.int()
.min(1)
.max(100)
.default(1),
limit: z.coerce
.number()
.int()
.min(1)
.max(50)
.default(20),
// Prevent unbounded queries
});
function validate<T>(schema: z.ZodSchema<T>) {
return (
req: Request,
res: Response,
next: NextFunction
) => {
const result = schema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
error: "Validation failed",
details: result.error.issues.map((issue) => ({
field: issue.path.join("."),
message: issue.message,
})),
});
return;
}
req.body = result.data;
next();
};
}
app.post(
"/api/users",
validate(createUserSchema),
async (req, res) => {
// req.body is now typed and validated
const { name, email } = req.body;
// Role assigned by backend logic, never from input
const role = "user";
await db.createUser({ name, email, role });
res.status(201).json({ name, email, role });
}
);Sicherheitsheader: Antworten absichern
Sicherheitsheader weisen Browser an, wie sie mit deinen Antworten umgehen sollen. Fehlende Header machen Clients anfällig für Clickjacking, XSS und Protocol-Downgrade-Angriffe.
function securityHeaders(
req: Request,
res: Response,
next: NextFunction
) {
// Prevent clickjacking
res.set("X-Frame-Options", "DENY");
// Block MIME-type sniffing
res.set("X-Content-Type-Options", "nosniff");
// Enable strict transport security
res.set(
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload"
);
// Content Security Policy for APIs
res.set(
"Content-Security-Policy",
"default-src 'none'; frame-ancestors 'none'"
);
// Control referrer information
res.set("Referrer-Policy", "strict-origin");
// Permissions policy
res.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()"
);
next();
}
app.use(securityHeaders);Output-Encoding: Antworten bereinigen
In deiner Datenbank gespeicherte Daten können schädliche Inhalte enthalten. Kodiere die Ausgabe, bevor du sie an Clients sendest.
// ❌ Raw database values in response
app.get("/api/comments/:postId", async (req, res) => {
const comments = await db.getComments(req.params.postId);
res.json(comments);
// If a comment contains <script>alert('xss')</script>
// and the client renders it as HTML — XSS
});// ✅ Sanitize output
import DOMPurify from "isomorphic-dompurify";
function sanitizeOutput<T extends Record<string, unknown>>(
obj: T,
htmlFields: string[] = []
): T {
const sanitized = { ...obj };
for (const [key, value] of Object.entries(sanitized)) {
if (typeof value === "string") {
if (htmlFields.includes(key)) {
// Allow safe HTML in designated fields
(sanitized as Record<string, unknown>)[key] =
DOMPurify.sanitize(value, {
ALLOWED_TAGS: [
"b",
"i",
"em",
"strong",
"a",
"p",
"br",
],
ALLOWED_ATTR: ["href"],
});
} else {
// Strip all HTML from non-HTML fields
(sanitized as Record<string, unknown>)[key] =
value
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}
}
}
return sanitized;
}
app.get(
"/api/comments/:postId",
async (req, res) => {
const comments = await db.getComments(
req.params.postId
);
const safe = comments.map((c) =>
sanitizeOutput(c, ["body"])
);
res.json(safe);
}
);Audit-Logging: Aufzeichnen, was passiert ist
Wenn ein Sicherheitsvorfall auftritt, verraten dir Audit-Logs, was passiert ist, wann und wer beteiligt war. Ohne sie ist die Reaktion auf Vorfälle reines Rätselraten.
interface AuditEvent {
timestamp: string;
userId: string | null;
action: string;
resource: string;
resourceId: string;
ip: string;
userAgent: string;
outcome: "success" | "failure" | "denied";
details?: Record<string, unknown>;
}
class AuditLogger {
async log(event: AuditEvent): Promise<void> {
// Write to append-only audit store
await db.query(
`INSERT INTO audit_log
(timestamp, user_id, action, resource,
resource_id, ip, user_agent, outcome, details)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
event.timestamp,
event.userId,
event.action,
event.resource,
event.resourceId,
event.ip,
event.userAgent,
event.outcome,
JSON.stringify(event.details ?? {}),
]
);
}
}
const audit = new AuditLogger();
// Audit middleware for sensitive operations
function auditAction(action: string, resource: string) {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
const originalJson = res.json.bind(res);
const startTime = new Date().toISOString();
res.json = function (body: unknown) {
const outcome =
res.statusCode >= 400 ? "failure" : "success";
audit.log({
timestamp: startTime,
userId: req.user?.id ?? null,
action,
resource,
resourceId:
req.params.id ?? "unknown",
ip: req.ip ?? "unknown",
userAgent:
req.headers["user-agent"] ?? "unknown",
outcome,
});
return originalJson(body);
};
next();
};
}
app.delete(
"/api/users/:id",
auditAction("delete", "user"),
async (req, res) => {
await db.deleteUser(req.params.id);
res.json({ deleted: true });
// Audit log automatically captures: who deleted
// which user, when, from what IP
}
);Wichtigste Erkenntnisse
Rate Limiting muss mehrschichtig und endpunktspezifisch sein: Authentifizierungsendpunkte brauchen strenge Limits (5 Versuche pro 15 Minuten), Suchendpunkte moderate Limits und Leseendpunkte können großzügiger sein — mit der authentifizierten Benutzer-ID als primärem Schlüssel und der IP als Fallback für nicht authentifizierte Anfragen. Die Eingabevalidierung mit Schema-Bibliotheken wie Zod sollte ungültige Anfragen an der Grenze ablehnen, bevor irgendwelche Geschäftslogik ausgeführt wird, und dabei Typbeschränkungen, Längenlimits und explizite Allowlists für aufgezählte Werte durchsetzen, ohne jemals vom Client gelieferte Rollen- oder Berechtigungsfelder zu vertrauen. Sicherheitsheader bilden eine passive Verteidigungsebene, deren Implementierung nichts kostet — Strict-Transport-Security, X-Content-Type-Options, Content-Security-Policy und X-Frame-Options verhindern ganze Angriffskategorien, unabhängig davon, ob die Anwendungslogik korrekt ist. Audit-Logging bei sensiblen Operationen schafft eine Untersuchungsspur, die die Reaktion auf Vorfälle von Rätselraten zu evidenzbasierter Analyse macht: Protokolliere, wer welche Aktion an welcher Ressource mit welchem Ergebnis ausgeführt hat, und speichere diese Aufzeichnungen in einem Append-only-Speicher getrennt von den Anwendungsdaten.


