Sicherheit
Das Framework liefert die Bausteine; diese Seite zeigt, wie man sie
zusammensetzt. Das lauffähige Gegenstück ist
examples/http/secure-service.ts.
Der empfohlene Stack
Abschnitt betitelt „Der empfohlene Stack“Die Reihenfolge zählt — von außen nach innen:
import { concat, cors, csrfProtection, fallback, handleErrors, path, requestId, requestTimeout, securityHeaders, withMiddleware, CorsOptions, CsrfOptions,} from 'actor-ts/http';
const corsOptions = CorsOptions.create().withOrigins('https://app.example').withCredentials();const csrf = csrfProtection(CsrfOptions.create().withSecret(process.env.CSRF_SECRET!));
const routes = withMiddleware(requestId(), withMiddleware(securityHeaders(), cors(corsOptions, withMiddleware(requestTimeout(15_000), withMiddleware(csrf, handleErrors(errorMapper, concat( appRoutes, fallback(notFound), )))))));| Schicht | Warum hier |
|---|---|
requestId | ganz außen, damit jede Log-Zeile (auch Fehler) eine ID hat |
securityHeaders | Header auf jeder Response setzen, auch bei Short-Circuits |
cors | außerhalb der Auth — Preflights sind laut Spec anonym |
requestTimeout | Latenz begrenzen, bevor echte Arbeit läuft |
csrf | nur Browser-Apps; nach CORS, vor den Handlern |
handleErrors | geworfene Fehler nahe den Routen auf Responses mappen |
fallback | an der Wurzel — beantwortet alles Unpassende |
Was jede Response ohnehin schon trägt
Abschnitt betitelt „Was jede Response ohnehin schon trägt“X-Content-Type-Options: nosniff braucht keine Einrichtung. Geschrieben
wird der Header vom Backend, nicht von einer Middleware — er erreicht
damit auch die Responses, die keine Middleware sieht: das backend-eigene
Fehler-Mapping, den fallback-404 und den 413 bei zu großem Body. Ein
Header aus dem Handler gewinnt weiterhin.
Er ist bewusst der einzige, der per Default an ist — es ist der einzige
Header des Bündels, der nichts daran ändern kann, wie eine bestehende
Anwendung eingebettet, geframed oder referenziert wird. X-Frame-Options
und Cross-Origin-Resource-Policy würden iframes, Cross-Origin-Einbettung
und OAuth-Popups brechen und bleiben deshalb Opt-in.
Um das ganze Bündel auf jede Response zu legen — inklusive derselben middleware-unsichtbaren Pfade — konfiguriere es am Server statt am Routen-Baum:
import { SecurityHeadersOptions } from 'actor-ts/http';
const securityHeadersOptions = SecurityHeadersOptions.create() .withFrameOptions('SAMEORIGIN') .withReferrerPolicy('strict-origin-when-cross-origin');
await http.newServerAt('0.0.0.0', 8080) .withSecurityHeaders(securityHeadersOptions) .bind(routes);Optionen zu übergeben ist ein Opt-in in das vollständige Bündel, samt
dessen eigener Defaults — die beiden Header oben kommen also zusammen mit
Cross-Origin-Resource-Policy: same-origin und dem Rest. Ein einfaches
Objekt geht genauso (withSecurityHeaders({ frameOptions: 'SAMEORIGIN' })),
und withSecurityHeaders(false) schaltet den Mechanismus komplett ab.
Serverweit oder als securityHeaders()-Middleware — beide setzen dieselbe
Menge. Zur Middleware greifst du, wenn nur ein Routen-Teilbaum sie braucht;
zum Builder, wenn der ganze Server sie braucht — denn die Middleware wird
überall dort übersprungen, wo eine Response nie durch sie zurückläuft.
Betriebsumfeld
Abschnitt betitelt „Betriebsumfeld“- TLS terminiert am Proxy. Setze HSTS trotzdem — Browser ignorieren es über reines HTTP. Gate Security-Header nicht an einem Scheme, das die App nicht sieht.
- Client-IPs vertrauen.
req.remoteAddressist der Socket-Peer, kein fälschbaresX-Forwarded-For. Vertraue einem Forwarded-Header nur, wenn dein Proxy ihn überschreibt (getClientIp-Option vonIpAllowlist). - Secrets aus der Umgebung, nicht aus HOCON-Dateien — das CSRF-Secret, Basic-Zugangsdaten und Bearer-Tokens nehmen alle Werte, die du beim Start lädst.
Cookies
Abschnitt betitelt „Cookies“Bevorzuge das __Host--Präfix (erfordert Secure, Path=/, kein Domain)
und SameSite=Lax oder Strict. Das CSRF-Cookie
ist bewusst nicht HttpOnly (Double-Submit braucht JS zum Lesen); dein
Session-/Auth-Cookie sollte HttpOnly sein. serializeCookie verweigert
Werte, die einen zweiten Header injizieren könnten.
Fehlerhygiene
Abschnitt betitelt „Fehlerhygiene“Interna nicht leaken. Nutze withErrorHandler (oder ein Top-Level
handleErrors), um einen generischen Body zurückzugeben, und logge das
Detail serverseitig, verschlüsselt über die Request-ID. Halte 403/404
unspezifisch — die Static-File-Schicht liefert für jeden abgelehnten Pfad
einen einheitlichen 404, verrät also nie, was existiert.
Redirects
Abschnitt betitelt „Redirects“redirect(target) nimmt ausschließlich Same-Origin-Ziele — eine
relative Referenz. Eine absolute URL, ein protokollrelatives //host oder
ein Steuerzeichen wirft HttpError(400); einen ?next=-Parameter direkt
in einen Redirect zu reichen, scheitert damit geschlossen, statt zum
Open-Redirect zu werden. Wenn das Verlassen des Origins Absicht ist, sag
es: redirectExternal(...) ist derselbe Helper ohne Origin-Regel, und der
eigene Name macht jeden bewussten Off-Origin-Sprung mit einem Befehl
auffindbar. Übergib eine Konstante oder ein Ziel aus deiner Allowlist —
nie einen rohen Request-Parameter. Siehe Route DSL.
Untrusted Input begrenzen
Abschnitt betitelt „Untrusted Input begrenzen“- Request-Bodies werden vom Backend gekappt (Fastify 1 MiB Default; Express/Hono ~10 MiB) — pro Backend an deine Payloads anpassen.
- WebSocket-Frames werden durch
maxFrameBytesgekappt (1 MiB Default). - Untrusted HTML — mit dem
html-Template escapen; für Rich-Markup vorrawHtmlmit einer dedizierten Bibliothek sanitisieren.
Statische Dateien
Abschnitt betitelt „Statische Dateien“Behalte die Defaults dotfiles: 'deny' und symlinks: 'within-root'.
Aktiviere browse nur für interne Tools — ein öffentliches Listing legt
deine Verzeichnisstruktur offen. Siehe Statische Dateien.
WebSocket
Abschnitt betitelt „WebSocket“Wickle eine websocket()-Route in cors(...), um Cross-Origin-Upgrades
abzuweisen, und lege Auth-Middleware darum — withMiddleware läuft zur
Upgrade-Zeit und gated damit den Handshake.
Checkliste
Abschnitt betitelt „Checkliste“| Maßnahme | Womit |
|---|---|
| MIME-Sniffing auf jeder Response abgeschaltet | nichts — per Default an |
| Security-Header auf jeder Response | securityHeaders(), oder serverweit withSecurityHeaders(...) |
| HTTPS vom Browser erzwungen | strictTransportSecurity() / withHsts |
| Cross-Origin kontrolliert | cors(...) |
| CSRF (Browser-Formulare) | csrfProtection(...) |
| Keine internen Fehler-Leaks | withErrorHandler + generische Bodies |
| Keine Open Redirects | redirect(...) — same-origin per Default |
| Request-Korrelation | requestId() |
| Begrenzte Latenz | requestTimeout(...) |
| Sichere Dateiauslieferung | getFromDirectory-Defaults (dotfiles deny, Symlink-Confinement) |
