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
@@ -0,0 +1,49 @@
|
||||
import { matchesFileSignature } from './file-signature';
|
||||
|
||||
describe('matchesFileSignature', () => {
|
||||
it('accepts a buffer starting with the PNG signature', () => {
|
||||
const png = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00,
|
||||
]);
|
||||
expect(matchesFileSignature(png, 'image/png')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a buffer starting with the JPEG signature', () => {
|
||||
const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
|
||||
expect(matchesFileSignature(jpeg, 'image/jpeg')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a RIFF/WEBP container', () => {
|
||||
const webp = Buffer.concat([
|
||||
Buffer.from('RIFF', 'ascii'),
|
||||
Buffer.from([0, 0, 0, 0]),
|
||||
Buffer.from('WEBP', 'ascii'),
|
||||
]);
|
||||
expect(matchesFileSignature(webp, 'image/webp')).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a PDF header', () => {
|
||||
const pdf = Buffer.from('%PDF-1.7\n', 'ascii');
|
||||
expect(matchesFileSignature(pdf, 'application/pdf')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a buffer whose bytes do not match the declared type', () => {
|
||||
const plainText = Buffer.from('this is not an image', 'ascii');
|
||||
expect(matchesFileSignature(plainText, 'image/png')).toBe(false);
|
||||
expect(matchesFileSignature(plainText, 'image/jpeg')).toBe(false);
|
||||
expect(matchesFileSignature(plainText, 'image/webp')).toBe(false);
|
||||
expect(matchesFileSignature(plainText, 'application/pdf')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a buffer shorter than the signature it claims to have', () => {
|
||||
expect(matchesFileSignature(Buffer.from([0xff, 0xd8]), 'image/jpeg')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('allows any bytes through for a mime type with no known signature', () => {
|
||||
expect(matchesFileSignature(Buffer.from('anything'), 'text/plain')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const SIGNATURE_CHECKS: Record<string, (buffer: Buffer) => boolean> = {
|
||||
'image/png': (buffer) =>
|
||||
buffer.length >= 8 &&
|
||||
buffer
|
||||
.subarray(0, 8)
|
||||
.equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
|
||||
'image/jpeg': (buffer) =>
|
||||
buffer.length >= 3 &&
|
||||
buffer[0] === 0xff &&
|
||||
buffer[1] === 0xd8 &&
|
||||
buffer[2] === 0xff,
|
||||
'image/webp': (buffer) =>
|
||||
buffer.length >= 12 &&
|
||||
buffer.subarray(0, 4).toString('ascii') === 'RIFF' &&
|
||||
buffer.subarray(8, 12).toString('ascii') === 'WEBP',
|
||||
'application/pdf': (buffer) =>
|
||||
buffer.length >= 5 && buffer.subarray(0, 5).toString('ascii') === '%PDF-',
|
||||
};
|
||||
|
||||
/**
|
||||
* Verifies a file's actual bytes match its declared MIME type instead of
|
||||
* trusting the client-supplied value alone (a multipart Content-Type header
|
||||
* or a data: URL prefix are both attacker-controlled). Used at every upload
|
||||
* path that persists the bytes to disk under a server-chosen name/extension,
|
||||
* so a mislabeled file can't be dropped onto the filesystem as a type it
|
||||
* isn't. Types with no signature defined here are not checked (returns
|
||||
* true) - callers still gate on their own MIME allowlist first.
|
||||
*/
|
||||
export function matchesFileSignature(
|
||||
buffer: Buffer,
|
||||
declaredMimeType: string,
|
||||
): boolean {
|
||||
const check = SIGNATURE_CHECKS[declaredMimeType];
|
||||
return check ? check(buffer) : true;
|
||||
}
|
||||
Reference in new issue
Block a user