Ein neues NestJS-Projekt zu übernehmen, kann sich wie die Öffnung einer Blackbox anfühlen. Selbst mit strikten Typisierungssystemen, Continuous Integration und umfassenden Test-Suiten bleibt oft ein blinder Fleck: die Abwesenheit von Sicherheitsvorkehrungen. Während Code-Reviews meist prüfen, was vorhanden ist, deckt statische Analyse auf, was fehlt – sei es ein fehlender Guard, ein vergessener Validator oder ein ungeschützter Endpunkt.
Ein kürzlich durchgeführter Scan mit dem Plugin eslint-plugin-nestjs-security auf einem 40.000 Zeilen umfassenden Produktivsystem lieferte in nur zwölf Sekunden 47 Verstöße. Darunter befanden sich sechs kritische Sicherheitslücken, die monatelang unentdeckt blieben. Kein Einzelfall: Solche Probleme entstehen nicht durch Nachlässigkeit, sondern weil moderne Frameworks wie NestJS mit ihrer deklarativen Syntax falsche Sicherheit suggerieren.
Unbewachte Controller – wenn Authentifizierung nur halb konfiguriert ist
Ein häufiger Irrtum: Entwickler gehen davon aus, dass eine globale Authentifizierung über JwtAuthGuard alle Routen automatisch schützt. Doch was, wenn diese Konfiguration durch Refactoring unterbrochen wurde? Genau das passierte in einem Projekt, dessen AppModule zwar den Guard in main.ts einband, ein sechs Monate zurückliegender Refactoring-Schritt jedoch die Middleware-Reihenfolge veränderte. Plötzlich waren kritische Admin-Routen wie /admin/users und /admin/user/:id ungeschützt – nicht weil der Guard fehlte, sondern weil die Routendefinition keinen expliziten Schutz erhielt.
Der Linter eslint-plugin-nestjs-security erkennt solche Lücken mit der Regel require-guards. Sie prüft strukturiert, ob jede Controller-Klasse oder Routenmethode entweder mit @UseGuards(...) geschützt ist oder explizit mit @Public() als öffentlich markiert wurde. Keine Typinferenz nötig – reine Syntaxanalyse.
// Vorher: Kein expliziter Schutz – Annahme: globaler Guard reicht
@Controller('admin')
export class AdminController {
@Get('users')
async getAllUsers() {
return this.usersService.findAll();
}
@Delete('user/:id')
async deleteUser(@Param('id') id: string) {
return this.usersService.delete(id);
}
}
// Nachher: Explizite Guards auf Controller-Ebene
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin')
async getAllUsers() {
return this.usersService.findAll();
}
@Delete('user/:id')
@Roles('admin')
async deleteUser(@Param('id') id: string) {
return this.usersService.delete(id);
}
}Sensible Daten in API-Antworten – wenn Entitäten unkontrolliert serialisiert werden
Eine weitere Schwachstelle zeigt sich, wenn ORM-Entitäten direkt als Antwort zurückgegeben werden – ohne Filterung sensibler Felder. In einem realen Fall enthielt die User-Entität neben der ID und E-Mail-Adresse auch password (als Hash) und refreshToken. Obwohl diese Entität ursprünglich nur intern über gRPC genutzt wurde, kam drei Monate später ein REST-Endpoint hinzu, der die Entität direkt serialisierte. Ein Penetrationstester entdeckte die Datenlecks durch einfache HTTP-Anfragen.
Das Problem: TypeScript schützt vor Laufzeitfehlern, nicht vor Datenlecks. Ohne explizite Exklusion werden alle Felder serialisiert – selbst wenn sie nur für interne Dienste gedacht sind. Die Lösung liegt in der Kombination aus class-transformer und einem globalen Interceptor:
import { Exclude } from 'class-transformer';
@Entity()
export class User {
@Column()
id: string;
@Column()
email: string;
@Column()
@Exclude()
password: string;
@Column()
@Exclude()
refreshToken: string;
}
// In main.ts aktivieren:
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));Auth-Endpunkte ohne Rate-Limiting – wenn Infrastruktur und Anwendung auseinanderlaufen
Authentifizierungsendpunkte wie /auth/login und /auth/reset-password sind besonders attraktive Angriffsziele. Doch selbst wenn Infrastruktur-Teams Rate-Limiting auf nginx-Ebene planen, kann ein einfacher Versionssprung im API-Pfad (z.B. von /api/v1/ zu /api/v2/) dazu führen, dass die Schutzmaßnahmen unwirksam werden.
Ein reales Beispiel: Die nginx-Konfiguration enthielt zwar eine Rate-Limiting-Regel für /api/v1/auth/login, doch parallel wurde im selben Sprint der API-Pfad auf /api/v2/ geändert – ohne dass beide Änderungen abgestimmt wurden. Das Ergebnis: Unbegrenzte Login-Versuche auf der neuen Route, bis ein Sicherheitsvorfall auftrat.
Die Lösung bietet das Plugin require-throttler, das prüft, ob jeder Auth-Controller oder jede Route mit @UseGuards(ThrottlerGuard) oder @Throttle(...) geschützt ist:
// Benötigt das Paket @nestjs/throttler
@Controller('auth')
@UseGuards(ThrottlerGuard)
export class AuthController {
@Post('login')
@Throttle({ default: { limit: 5, ttl: 60000 } }) // 5 Anfragen pro Minute
async login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
}Unvalidierte DTO-Eingaben – wenn TypeScript-Typen falsche Sicherheit vermitteln
Ein klassisches Missverständnis: Viele Entwickler gehen davon aus, dass eine typisierte DTO wie CreatePostDto automatisch validiert wird. Doch TypeScript-Typen existieren nur zur Compile-Zeit. Zur Laufzeit akzeptiert das Framework jeden Input – selbst wenn dieser nicht dem erwarteten Schema entspricht.
Ohne den Einsatz von ValidationPipe oder expliziten Validatoren in den Parametern werden beispielsweise bei einem POST-Request an /posts/typed beliebige JSON-Strukturen akzeptiert, solange sie der TypeScript-Definition entsprechen. Ein Angreifer könnte etwa zusätzliche Felder wie admin: true injizieren, die im Backend nicht verarbeitet, aber dennoch akzeptiert werden.
Die Lösung liegt in der Kombination aus globaler oder parametereigener Validierung:
// Option 1: Globale Validierung (empfohlen)
// In main.ts:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Unerwartete Felder werden entfernt
forbidNonWhitelisted: true // Unerwartete Felder führen zu Fehlern
})
);
// Option 2: Parametereigene Validierung
@Post()
async createPost(
@Body(new ValidationPipe())
body: CreatePostDto
) {
return this.postsService.create(body);
}Fehlende Validierungsdekoratoren – wenn nur ein Teil der DTO validiert wird
Ein weiterer häufiger Fehler: Entwickler fügen zwar @IsEmail() für E-Mail-Felder hinzu, vergessen aber, andere Felder wie name, role oder age zu validieren. Das Problem verschärft sich, wenn später neue Felder hinzugefügt werden – etwa ein role-Feld, das sowohl "admin" als auch "user" akzeptieren soll. Ohne explizite Validierung können Angreifer beliebige Werte injizieren, selbst wenn der Rest der DTO korrekt validiert wird.
Die Regel require-class-validator des Plugins prüft, ob jedes Feld in einer DTO-Klasse mit einem passenden Validator wie @IsString(), @IsIn(['admin', 'user']), @Min(18) oder ähnlichen Dekoratoren versehen ist. Nur so lässt sich sicherstellen, dass alle Eingaben zur Laufzeit geprüft werden.
Fazit: Automatisierung als Sicherheitsnetz für Entwicklerteams
Die sechs geschilderten Fälle zeigen: Selbst in gut gewarteten NestJS-Projekten schlummern Sicherheitsrisiken, die typische Code-Reviews und manuelle Tests übersehen. Statische Analysetools wie eslint-plugin-nestjs-security ergänzen bestehende Sicherheitsprozesse, indem sie auf Abwesenheiten – fehlende Guards, unvalidierte Eingaben, exponierte Daten – hinweisen.
Der Schlüssel liegt darin, solche Checks frühzeitig in den Entwicklungsprozess zu integrieren: als Teil der CI-Pipeline, vor jedem Merge oder sogar lokal als Pre-Commit-Hook. Denn Sicherheit ist kein Zustand, den man einmal erreicht – sie muss kontinuierlich geprüft und verbessert werden, bevor sie zum Problem wird.
KI-Zusammenfassung
NestJS projenizde statik analiz araçlarını kullanmaya başlamak, kod inceleme sırasında gözden kaçan ciddi güvenlik açıklarını ortaya çıkarabilir. Bu makalede, 40 bin satırlık bir kod tabanında tespit edilen 6 farklı güvenlik açığı ve onları düzeltmenin pratik yolları ele alınıyor.