TL;DR: Eine Single Page Application liefert ihre Routenkarte selbst aus: Routentabelle, Guard-Namen und Umgebungskonfiguration liegen im Client-Bundle. Ich habe alles mit einem einzigen Skript-Download extrahiert.
Ausgangslage
Ich habe das Ziel als Black-Box geprüft: keine Zugangsdaten, nur die öffentlich sichtbare Fläche. Das ist, was ich sah, bevor ich etwas angefasst habe.
Ein Portal einer Industrie- und Handelskammer auf Basis eines modernen Komponentenframeworks, ausgeliefert als vorgerenderte Anwendung über ein Content Delivery Network. Im Fokus stand, was die ausgelieferten Client-Assets über Routen, Umgebungen und Verwaltungsfunktionen preisgeben.
Ich habe alle identifizierenden Informationen entfernt. Das Ziel wird ausschließlich nach Branche und Technologieklasse beschrieben, damit das Muster übertragbar bleibt.
Das Muster
Das Muster, das ich erkannt habe — und das ich in ähnlichen Landschaften immer wieder finde:
Prerendering hilft Suchmaschinen und tut nichts für Vertraulichkeit. Der Router, der entscheidet, was ein Nutzer erreichen darf, ist ein Client-Modul. Damit liefern die vollständige Routentabelle, die lazy geladenen Feature-Chunks und die Umgebungskonfiguration an jeden Besucher.
Das dreht die Ökonomie der Aufklärung um. Wo ein klassisches Ziel ein Verzeichnis-Scan kostet, liefert ein clientseitig gerendertes Ziel dieselbe Information aus einem einzigen Skript-Download. Relevant ist nicht die Größe, sondern die Lesbarkeit: Pfadstrings, Berechtigungs-Guards, Rollennamen und API-Basis-URLs überleben die Minification.
Befunde
| Schweregrad | Befund | Nachweis |
|---|---|---|
| MITTEL | Vollständige Routentabelle im Client-Bundle lesbar | Administrative und interne Ansichten aus Assets enumeriert |
| MITTEL | Namen der Berechtigungs-Guards preisgegeben | Die vorgesehene Autorisierungslogik im Bundle beschrieben |
| MITTEL | Umgebungskonfiguration ins Bundle kompiliert | API-Basisadressen pro Umgebung |
| NIEDRIG | Source Map öffentlich ausgeliefert | Originalquellen rekonstruierbar |
| NIEDRIG | Lazy Chunks über vorhersagbare Namen enumerierbar | Feature-Module einzeln ladbar |
| INFO | Serverseitige Fläche minimal | Kein serverseitiges Rendering und keine Datenbank erreichbar |
Der Angriffspfad
Reproduktion im Labor
Jeder Befehl unten zielt auf einen Lab-Container, den ich selbst kontrolliere. Nichts davon ist auf ein laufendes System gerichtet.
Die Routenkarte aus dem eigenen Bundle lesen
# Ziel: Ihre eigenen Build-Assets
$ curl -s https://example.org/ | grep -oE '/assets/[A-Za-z0-9._-]+\.js' | sort -u | head
$ for f in $(curl -s https://example.org/ | grep -oE '/assets/[A-Za-z0-9._-]+\.js' | sort -u); do
curl -s "https://example.org$f"
done > /tmp/bundle.txt
# routenähnliche Strings überleben die Minification
$ grep -oE '"/[a-z0-9/-]{4,40}"' /tmp/bundle.txt | sort -u | head -40
# Guard- und Rollenkennungen
$ grep -oiE '(canActivate|hasRole|isAdmin|requiresAuth|permission)[A-Za-z]*' /tmp/bundle.txt | sort -u | head
Erkennung in eigener Infrastruktur
Prüfen, ob Source Maps und Umgebungsdateien ausgeliefert werden
$ curl -so /dev/null -w '%{http_code}\n' https://example.org/assets/main.js.map
$ curl -so /dev/null -w '%{http_code}\n' https://example.org/environments/environment.prod.js
$ curl -so /dev/null -w '%{http_code}\n' https://example.org/assets/config.json
Auf Legacy-CMS-Routen auf demselben Host achten
$ for p in /wp-json/ /xmlrpc.php /api/graphql /.well-known/security.txt; do
printf '%s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://example.org$p)"
done
Behebung
Source Maps nicht in Produktion ausliefern
{
"production": {
"sourceMap": false,
"namedChunks": false
}
}
Umgebungswerte auf dem Server halten, nicht im Bundle
// das Bundle darf keine umgebungspezifischen Endpunkte enthalten
// nur injizieren, was der Browser wirklich braucht, und nichts, das Ihre Landschaft kartiert
export const environment = {
production: true,
apiBase: '/api', // relativer Same-Origin-Pfad, niemals ein vollständiger Host
};
Autorisierung auf dem Server durchsetzen, den Guard nur als UX behandeln
// der Client-Guard verbessert die Erfahrung, der Server entscheidet
// jede administrative Route braucht dieselbe serverseitige Sitzungsprüfung
export const canActivate = () => {
const session = inject(SessionService);
return session.hasRole('admin') ? true : inject(Router).parseUrl('/login');
};
Sobald ein Angreifer Zugriff hat, kann er folgende Aktionen durchführen:
Fazit — was ich daraus mitnehme
- Clientseitige Routing-Guards sind Benutzererlebnis, niemals Autorisierung.
- Eine Routentabelle im Bundle ist eine Angriffsflächenkarte ohne Ablaufdatum.
- Source Maps in Produktion abschalten und aufhören, Besuchern Ihre Landschaft zu beschreiben.
- Achten Sie auf ein Legacy-CMS auf demselben Host.