fix: SSO-MFA-Umgehung, Datei-Magic-Bytes und oeffentliches Swagger geschlossen
Weitere vom Sicherheitsaudit gemeldete Luecken behoben, die bisher als niedrige Prioritaet offen gelassen wurden: - SSO-Login (Entra ID) prüft jetzt lokale MFA genauso wie der Passwort-Login: ist mfaEnabled gesetzt, liefert /auth/sso/complete denselben mfaRequired-Codepfad wie /auth/login, bevor Tokens ausgestellt werden. Frontend (Login-Seite, AuthContext) zeigt in dem Fall den bestehenden Code-Eingabe-Schritt statt die Anmeldung stillschweigend abzuschliessen. - Avatar- und Dokument-Uploads pruefen jetzt die tatsaechlichen Datei-Bytes (Magic Numbers fuer PNG/JPEG/WEBP/PDF) statt nur dem client-angegebenen MIME-Typ/data:-URL-Praefix zu vertrauen. - Swagger-UI (/api/v1/docs) ist jetzt standardmaessig deaktiviert (ENABLE_SWAGGER=true erforderlich), da sie zuvor das komplette API-Schema ungeschuetzt oeffentlich preisgab. npm-audit-Warnungen (deepmerge-ts/mysql2 unter prisma) bewusst nicht gefixt: kein stabiles Prisma-Release loest sie bislang auf, nur eine Release-Candidate-Hauptversion - das Risiko eines Downgrades/RC-Upgrades der Datenbankschicht uebersteigt den Nutzen fuer eine Kette, die zur Laufzeit ohnehin nicht erreichbar ist (nur Prisma-CLI, nur MySQL-Treiber, dieses Projekt nutzt Postgres). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
1b16b49913
commit
b7ff92abd3
16 files changed
+225
-28
No files matched your search
@@ -111,11 +111,25 @@ describe("LoginPage", () => {
|
||||
|
||||
it("completes SSO login and redirects to the dashboard when returning with ssoPending", async () => {
|
||||
searchParamsMock.mockReturnValue(new URLSearchParams("ssoPending=1"));
|
||||
completeSsoMock.mockResolvedValueOnce(undefined);
|
||||
completeSsoMock.mockResolvedValueOnce({ mfaRequired: false });
|
||||
render(<LoginPage />);
|
||||
|
||||
expect(await screen.findByText(/microsoft-anmeldung wird abgeschlossen/i)).toBeInTheDocument();
|
||||
await vi.waitFor(() => expect(replaceMock).toHaveBeenCalledWith("/dashboard"));
|
||||
expect(completeSsoMock).toHaveBeenCalled();
|
||||
expect(replaceMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
|
||||
it("shows the MFA code step when SSO completion still requires local MFA", async () => {
|
||||
searchParamsMock.mockReturnValue(new URLSearchParams("ssoPending=1"));
|
||||
completeSsoMock.mockResolvedValueOnce({ mfaRequired: true, mfaToken: "sso-mfa-token" });
|
||||
verifyMfaMock.mockResolvedValueOnce(undefined);
|
||||
render(<LoginPage />);
|
||||
|
||||
const codeInput = await screen.findByLabelText("Code");
|
||||
expect(replaceMock).toHaveBeenCalledWith("/login");
|
||||
await userEvent.type(codeInput, "123456");
|
||||
await userEvent.click(screen.getByRole("button", { name: /bestätigen/i }));
|
||||
|
||||
expect(verifyMfaMock).toHaveBeenCalledWith("sso-mfa-token", "123456");
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
@@ -51,7 +51,11 @@ function LoginPageContent() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [ssoEnabled, setSsoEnabled] = useState(false);
|
||||
const [ssoLoginHref, setSsoLoginHref] = useState<string | null>(null);
|
||||
const isCompletingSso = ssoPending;
|
||||
// Its own state rather than deriving straight from the ssoPending query
|
||||
// param: once completeSso() resolves with an MFA challenge, the MFA form
|
||||
// must show immediately (same render pass as setMfaToken), without
|
||||
// waiting on a router.replace() round trip to first clear the param.
|
||||
const [isCompletingSso, setIsCompletingSso] = useState(ssoPending);
|
||||
|
||||
useEffect(() => {
|
||||
ssoStatusRequest().then(setSsoEnabled);
|
||||
@@ -66,8 +70,17 @@ function LoginPageContent() {
|
||||
useEffect(() => {
|
||||
if (ssoPending) {
|
||||
completeSso()
|
||||
.then(() => router.replace("/dashboard"))
|
||||
.then((outcome) => {
|
||||
setIsCompletingSso(false);
|
||||
if (outcome.mfaRequired) {
|
||||
setMfaToken(outcome.mfaToken);
|
||||
router.replace("/login");
|
||||
} else {
|
||||
router.replace("/dashboard");
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setIsCompletingSso(false);
|
||||
setError("SSO-Anmeldung fehlgeschlagen, bitte erneut versuchen.");
|
||||
router.replace("/login");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user