Mit der Anmeldung aus 0301 trägt die SPA ein Token. Diese Lektion fragt, wer es lesen und wer die API aus fremden Seiten aufrufen kann: CORS an der API, Angulars Bereinigung gegen XSS, die Content-Security-Policy, XSRF und die Abhängigkeiten. Der Teil der Kladde: die CORS-Richtlinie in Kladde.Api, eine Notiz-Anzeige in src/app/sicherheit und angular.json.

Stand der Prüfung. Ausgeführt (Node 24.22.3, Angular 22.2, .NET 10.0.400): die Probe-API mit CORS-Richtlinie lief lokal, die Vorabanfragen unten sind echte curl-Läufe, ihre Köpfe stehen unverändert da. Die XSS-Tests liefen mit Vitest und jsdom (ng test --no-watch Exit-Code 0, 3 Tests in 2 Dateien). ng build mit security.autoCsp endete mit 0, das erzeugte index.html ist gelesen (Hash gekürzt). npm audit: Exit-Code 0, „found 0 vulnerabilities“; npm ci --dry-run Exit-Code 0. Nicht ausgeführt: ein Browser (also auch kein echter CORS-Fehler in der Konsole, keine Ausführung von onerror), eine CSP mit Nonce, Trusted Types, XSRF mit einer Cookie-Anmeldung. Aussagen über Angular stammen von angular.dev (Seite „Security“, Arbeitsbereichs-Konfiguration), über CORS aus Learn („Enable Cross-Origin Requests (CORS) in ASP.NET Core“), am Schreibtag gelesen.

CORS: nur wenn die Herkunft wechselt

Eine Herkunft (Origin) besteht aus Schema, Host und Port. http://localhost:4200 und https://localhost:7180 sind zwei Herkünfte. Der Browser erlaubt einer Seite nicht, die Antwort einer anderen Herkunft zu lesen (Same-Origin-Policy); CORS ist die Erlaubnis, die der Server dazu gibt. Zwei Aussagen der Seite zuerst: CORS ist kein Sicherheitsmerkmal, sondern lockert eine Sicherheitsregel („An API is not safer by allowing CORS“), und der Server führt die Anfrage trotzdem aus: Der Browser verwirft die Antwort. curl oder ein .NET-Client sind davon nicht berührt.

Die SPA aus 0101 ruft /api/... relativ auf, und der Dev-Server leitet über den Proxy weiter: Für den Browser ist das dieselbe Herkunft, CORS kommt nicht vor. Nötig wird es, sobald die SPA ihre API unter anderer Adresse ruft, etwa wenn SPA und API in Azure getrennt liegen (0403) oder wenn Sie im Dev-Server ohne Proxy gegen https://localhost:7180 rufen.

Die Richtlinie

C#
var origins = builder.Configuration.GetSection("Cors:Origins").Get<string[]>() ?? [];
builder.Services.AddCors(options => options.AddPolicy("kladde-spa", policy => policy
    .WithOrigins(origins)
    .WithHeaders("Authorization", "Content-Type")
    .WithMethods("GET", "POST", "PUT", "DELETE")));

var app = builder.Build();
app.UseRouting();
app.UseCors("kladde-spa");
app.UseAuthentication();
app.UseAuthorization();

In appsettings.json steht "Cors": { "Origins": [ "http://localhost:4200" ] }. Die Regeln der Seite: Die Herkunft hat keinen Schrägstrich am Ende, sonst gibt der Vergleich false und es kommt kein Kopf zurück; UseCors steht nach UseRouting und vor UseAuthorization. In der Probe steht es auch vor UseAuthentication: Das hat einen Grund, siehe die Läufe unten. AllowAnyOrigin ist laut Seite unsicher, und zusammen mit AllowCredentials eine unsichere Einstellung; die Kladde braucht beides nicht, weil das Token im Kopf Authorization reist, nicht als Cookie.

Die Vorabanfrage bei Bearer

Der Browser fragt vor der eigentlichen Anfrage mit OPTIONS, wenn die Anfrage nicht „einfach“ ist: Die Seite nennt als Bedingung, dass keine anderen Köpfe gesetzt werden als Accept, Accept-Language, Content-Language, Content-Type und Last-Event-ID (und eine Methode GET, HEAD, POST). Ein Kopf Authorization fällt heraus: Jeder Aufruf der Kladde-API mit Token löst eine Vorabanfrage aus. Die Läufe gegen die Probe auf http://localhost:5188, je curl.exe -i -X OPTIONS mit den genannten Köpfen:

# A: Origin http://localhost:4200, Access-Control-Request-Headers: authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Headers: Authorization,Content-Type
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Origin: http://localhost:4200

# B: Origin http://localhost:4300 (falsche Herkunft)
HTTP/1.1 204 No Content
(keine Access-Control-Köpfe)

# C: Origin http://localhost:4200/ (mit Schrägstrich)
HTTP/1.1 204 No Content
(keine Access-Control-Köpfe)

Der Lauf bestätigt, was die Seite sagt: Eine abgelehnte Vorabanfrage bekommt trotzdem 204, nur ohne die CORS-Köpfe, und der Browser lehnt dann den Aufruf ab. Ein falscher Origin ist also nicht an einem Statuscode zu erkennen, sondern am Fehlen der Köpfe. Ein GET ohne Token mit Origin: http://localhost:4200 ergab 401 mit Access-Control-Allow-Origin und WWW-Authenticate: Bearer: Weil UseCors vor der Authentifizierung steht, sieht die SPA ein echtes 401, keinen CORS-Fehler. Ein Lauf mit einem zusätzlich angefragten Kopf x-eigen ergab dieselbe Antwort wie A; die Seite beschreibt hier eine Ablehnung. Ob der Browser den Aufruf dann blockiert, ist ohne Browser nicht geprüft: offen.