Befunde der Sicherheitspruefung abarbeiten
S1 (hoch): Die OIDC-Rueckkehr verknuepft eine Identitaet mit einem bestehenden Konto nur bei bestaetigter E-Mail — geprueft wurde das auch, nur war die Bestaetigung als `email_verified !== false` berechnet. Damit galt auch ein fehlender Claim als bestaetigt, und genau den schicken manche Anbieter gar nicht. Jetzt zaehlt allein ein ausdrueckliches `true`. S2 (hoch): `isAuthEnabled()` verlangte den Wert "true"; alles andere hiess "aus". Eine vergessene Variable ergab eine Anwendung, in der jeder Besucher alles darf, und nichts sah danach aus. Umgedreht: Abgeschaltet wird nur, wer ausdruecklich "false" setzt. Die Vorgaben in Compose und .env.example stehen auf true, und der Container warnt beim Start mehrzeilig, solange die Anmeldung aus ist. S5 (mittel): Der OIDC-Bootstrap las `user.count()` und legte danach an — zwei gleichzeitige erste Anmeldungen konnten beide Administrator werden. Jetzt in einer serialisierbaren Transaktion, wie die lokale Ersteinrichtung es laengst tut. S6 (niedrig): Die Herkunftspruefung verglich nur den Host, nicht das Schema. Das Schema kommt jetzt aus BASE_URL — die eine Angabe zum Ursprung, ueber die der Aufrufer nicht mitbestimmt. S3, S4 und S7 bleiben offen und stehen begruendet in docs/sicherheit.md. Bei S3 wurde der naheliegende Ausweg geprueft und verworfen: Waehrend der Sperre weiter Passwoerter zu pruefen kostet 128 MiB je Versuch und tauschte eine Sperre gegen ein Konto gegen eine gegen alle. Refs #20
This commit is contained in:
@@ -23,9 +23,14 @@ export type Actor =
|
||||
* Bewusst eine Umgebungsvariable und kein Schalter in der Oberfläche: so
|
||||
* kann man sich nicht aussperren. Wer den Zugang verliert, setzt
|
||||
* `AUTH_ENABLED=false` in der .env und startet neu.
|
||||
*
|
||||
* **Abgeschaltet wird nur auf ausdrückliche Ansage.** Ein fehlender oder
|
||||
* leerer Wert bedeutet „an“. Vorher war es umgekehrt: Wer die Variable
|
||||
* vergaß, bekam eine Anwendung, in der jeder Besucher alles darf — und
|
||||
* merkte es nicht, weil nichts danach aussah.
|
||||
*/
|
||||
export function isAuthEnabled(): boolean {
|
||||
return (process.env.AUTH_ENABLED ?? "").trim().toLowerCase() === "true";
|
||||
return (process.env.AUTH_ENABLED ?? "").trim().toLowerCase() !== "false";
|
||||
}
|
||||
|
||||
export async function getActor(): Promise<Actor> {
|
||||
|
||||
@@ -326,7 +326,14 @@ export async function exchangeCode(options: {
|
||||
return {
|
||||
subject,
|
||||
email,
|
||||
emailVerified: payload.email_verified !== false,
|
||||
// Nur ein ausdrückliches `true` zählt. Ein fehlender Claim, `null`, `0`
|
||||
// oder die Zeichenkette „true" gelten als **nicht** bestätigt.
|
||||
//
|
||||
// Vorher stand hier `!== false`, und damit war das Gegenteil der Fall:
|
||||
// Ein Anbieter, der den Claim gar nicht schickt, hätte die Verknüpfung
|
||||
// mit einem bestehenden Konto erlaubt — genau das, was die Prüfung in
|
||||
// der Callback-Route verhindern soll.
|
||||
emailVerified: payload.email_verified === true,
|
||||
name,
|
||||
groups: readGroups(payload, config.groupsClaim),
|
||||
};
|
||||
|
||||
@@ -26,6 +26,15 @@ export function isSameOrigin(headers: Headers): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Das Schema kommt aus `BASE_URL`, nicht aus der Anfrage: Es ist die eine
|
||||
// Angabe zum Ursprung, über die der Aufrufer nicht mitbestimmt. Ohne diesen
|
||||
// Vergleich gälte `http://example.com` als eigen, obwohl die Anwendung nur
|
||||
// unter `https://example.com` läuft.
|
||||
const erwartetesSchema = (process.env.BASE_URL ?? "").startsWith("http://")
|
||||
? "http:"
|
||||
: "https:";
|
||||
if (herkunft.protocol !== erwartetesSchema) return false;
|
||||
|
||||
const host = headers.get("x-forwarded-host") ?? headers.get("host");
|
||||
return Boolean(host) && herkunft.host === host;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user