feat(cert-manager): Hersteller-ZIP, PKCS#7, eingefügter Text, Analysieren und Aufteilen
- ZIP wird an den Anfangsbytes erkannt und mit Grenzen geöffnet (eine Ebene, Verhältnis, Gesamtgröße, verschlüsselte Einträge) - PKCS#7 als PEM und DER, auch für EC, über den ASN.1-Lauf - Eingefügter PEM-Text als eigener Eintrag im Reiter Dateien - Neue Reiter Analysieren und Aufteilen auf dem gemeinsamen Arbeitsbereich Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import AdmZip from 'adm-zip';
|
||||
import { cleanSourcePath } from './cert-names';
|
||||
import type { IgnoredEntry } from './cert-types';
|
||||
|
||||
/**
|
||||
* ZIP-Erkennung und -Entpacken des Zertifikat-Managers (quick-261009-ikt, D-17).
|
||||
*
|
||||
* Regeln:
|
||||
* - Ein ZIP erkennt man an den Anfangsbytes (PK\x03\x04 oder PK\x05\x06), nie an der Endung.
|
||||
* - Nur eine Ebene: ein Eintrag, der selbst ein ZIP ist, wird mit Grund `nestedZip` gemeldet.
|
||||
* - Muell wird still uebersprungen: Ordner, `__MACOSX/`, Namen mit fuehrendem Punkt, Thumbs.db,
|
||||
* desktop.ini. Er zaehlt auch nicht zur Eintragsgrenze.
|
||||
* - Mehr als `maxEntries` Eintraege: das ganze ZIP wird mit `tooManyEntries` abgelehnt.
|
||||
* - Verschluesselter Eintrag (Flag Bit 0): `encryptedZip`. Deklarierte Groesse ueber `maxEntryBytes`:
|
||||
* `tooLarge`. Deklarierte Groesse / max(gepackt, 1) ueber `maxRatio`: `suspicious`.
|
||||
* - Summe der deklarierten Groessen der behaltenen Eintraege ueber `maxTotalBytes`: das ganze ZIP
|
||||
* wird mit `zipTooLarge` abgelehnt.
|
||||
* - Alle diese Pruefungen laufen auf den Kopfdaten und damit VOR dem ersten Entpacken.
|
||||
* adm-zip entpackt hoechstens die deklarierte Groesse und prueft die CRC; zusaetzlich wird das
|
||||
* Ergebnis noch einmal gegen die Grenze geprueft.
|
||||
* - Eintragsnamen dienen nur der Anzeige (Steuerzeichen entfernt, hoechstens 255 Zeichen) und
|
||||
* werden nie als Dateipfad benutzt.
|
||||
*/
|
||||
|
||||
export interface ZipLimits {
|
||||
maxEntries: number;
|
||||
maxEntryBytes: number;
|
||||
maxRatio: number;
|
||||
maxTotalBytes: number;
|
||||
}
|
||||
|
||||
export const ZIP_LIMITS: ZipLimits = {
|
||||
maxEntries: 100,
|
||||
maxEntryBytes: 1024 * 1024,
|
||||
maxRatio: 100,
|
||||
maxTotalBytes: 20 * 1024 * 1024,
|
||||
};
|
||||
|
||||
export interface ZipBlob {
|
||||
/** Anzeigepfad: "zipname/eintrag" */
|
||||
path: string;
|
||||
buffer: Buffer;
|
||||
}
|
||||
|
||||
export interface ZipExpansion {
|
||||
blobs: ZipBlob[];
|
||||
ignored: IgnoredEntry[];
|
||||
}
|
||||
|
||||
/** ZIP-Anfangsbytes: lokaler Dateikopf (PK 03 04) oder leeres Archiv (PK 05 06). */
|
||||
export function isZip(buffer: Buffer): boolean {
|
||||
return (
|
||||
buffer.length >= 4 &&
|
||||
buffer[0] === 0x50 &&
|
||||
buffer[1] === 0x4b &&
|
||||
((buffer[2] === 0x03 && buffer[3] === 0x04) || (buffer[2] === 0x05 && buffer[3] === 0x06))
|
||||
);
|
||||
}
|
||||
|
||||
const JUNK_FILES = new Set(['thumbs.db', 'desktop.ini']);
|
||||
|
||||
function isJunk(entryName: string, isDirectory: boolean): boolean {
|
||||
if (isDirectory) return true;
|
||||
const segments = entryName.split(/[\\/]/).filter(Boolean);
|
||||
if (segments.length === 0) return true;
|
||||
if (segments.some((s) => s === '__MACOSX' || s.startsWith('.'))) return true;
|
||||
return JUNK_FILES.has(segments[segments.length - 1].toLowerCase());
|
||||
}
|
||||
|
||||
function displayPath(zipName: string, entryName: string): string {
|
||||
return cleanSourcePath(`${zipName}/${entryName.replace(/^[\\/]+/, '')}`);
|
||||
}
|
||||
|
||||
export function expandZip(
|
||||
buffer: Buffer,
|
||||
zipName: string,
|
||||
file: number,
|
||||
limits: ZipLimits = ZIP_LIMITS,
|
||||
): ZipExpansion {
|
||||
const blobs: ZipBlob[] = [];
|
||||
const ignored: IgnoredEntry[] = [];
|
||||
const whole = (reason: IgnoredEntry['reason']): ZipExpansion => ({
|
||||
blobs: [],
|
||||
ignored: [{ file, path: cleanSourcePath(zipName), reason }],
|
||||
});
|
||||
|
||||
let entries: ReturnType<AdmZip['getEntries']>;
|
||||
try {
|
||||
entries = new AdmZip(buffer).getEntries();
|
||||
} catch {
|
||||
return whole('brokenZip');
|
||||
}
|
||||
|
||||
const candidates = entries.filter((e) => !isJunk(e.entryName, e.isDirectory));
|
||||
if (candidates.length > limits.maxEntries) return whole('tooManyEntries');
|
||||
|
||||
// Kopfdaten pruefen, bevor irgendein Eintrag entpackt wird.
|
||||
const kept: typeof candidates = [];
|
||||
let total = 0;
|
||||
for (const entry of candidates) {
|
||||
const path = displayPath(zipName, entry.entryName);
|
||||
const size = entry.header.size;
|
||||
if (entry.header.encrypted) {
|
||||
ignored.push({ file, path, reason: 'encryptedZip' });
|
||||
} else if (entry.entryName.toLowerCase().endsWith('.zip')) {
|
||||
ignored.push({ file, path, reason: 'nestedZip' });
|
||||
} else if (size > limits.maxEntryBytes) {
|
||||
ignored.push({ file, path, reason: 'tooLarge' });
|
||||
} else if (size / Math.max(entry.header.compressedSize, 1) > limits.maxRatio) {
|
||||
ignored.push({ file, path, reason: 'suspicious' });
|
||||
} else {
|
||||
kept.push(entry);
|
||||
total += size;
|
||||
}
|
||||
}
|
||||
if (total > limits.maxTotalBytes) return whole('zipTooLarge');
|
||||
|
||||
for (const entry of kept) {
|
||||
const path = displayPath(zipName, entry.entryName);
|
||||
let data: Buffer;
|
||||
try {
|
||||
data = entry.getData();
|
||||
} catch {
|
||||
ignored.push({ file, path, reason: 'brokenZip' });
|
||||
continue;
|
||||
}
|
||||
if (data.length > limits.maxEntryBytes) {
|
||||
ignored.push({ file, path, reason: 'tooLarge' });
|
||||
} else if (isZip(data)) {
|
||||
ignored.push({ file, path, reason: 'nestedZip' });
|
||||
} else {
|
||||
blobs.push({ path, buffer: data });
|
||||
}
|
||||
}
|
||||
return { blobs, ignored };
|
||||
}
|
||||
Reference in New Issue
Block a user