API-Datei-Uploads mit Magic Numbers absichern
Die Validierung von Datei-Uploads ist entscheidend für die API-Sicherheit. Verlassen Sie sich ausschließlich auf Dateiendungen oder MIME-Typen, bleibt Ihre Anwendung anfällig für Spoofing-Angriffe. Eine sicherere Methode besteht darin, die Magic Numbers einer Datei zu prüfen, auch bekannt als Dateisignaturen.
Warum die Prüfung der Dateiendung nicht ausreicht
Dateiendungen und MIME-Typen lassen sich leicht fälschen. Angreifer können schädliche Dateien so
umbenennen, dass sie harmlos wirken, und umgehen damit einfache Validierungsprüfungen. Eine
schädliche ausführbare Datei könnte beispielsweise in document.pdf umbenannt
werden und so Systeme täuschen, die nur die Dateiendung prüfen.
Die Validierung des MIME-Typs ist ebenso unzuverlässig, da Browser und Clients beliebige Header vom
Typ Content-Type setzen können. Damit ist die Validierung der Magic Numbers für
eine belastbare Dateiverifizierung unverzichtbar.
Magic Numbers und Dateisignaturen verstehen
Magic Numbers sind erkennbare Byte-Folgen, die häufig nahe am Dateianfang stehen und dabei helfen, das wahrscheinliche Format einer Datei zu bestimmen. Sie sind unabhängig vom Dateinamen und vom MIME-Typ, den der Client angibt, doch auch sie lassen sich von Angreifern fälschen. Eine übereinstimmende Signatur beweist weder, dass die vollständige Datei gültig ist, noch, dass sie sich gefahrlos verarbeiten lässt. Betrachten Sie sie als eine Ebene einer Upload-Richtlinie.
Gängige Magic Numbers
Nachfolgend finden Sie eine kurze, aber häufig verwendete Liste. Einen umfassenden Überblick bietet Wikipedias „List of file signatures“.
| Format | Hex (Offset 0) | Hinweise |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | Immer 8 Byte |
| JPEG | FF D8 FF DBFF D8 FF E0FF D8 FF E1 | Deckt JFIF- und EXIF-Varianten ab |
| GIF | 47 49 46 38 37 61 (GIF87a)47 49 46 38 39 61 (GIF89a) | Sechs Byte |
25 50 44 46 2D (%PDF-) | Fünf Byte | |
| ZIP | 50 4B 03 0450 4B 05 06 | DOCX, ODT und APK sind ZIP-Container |
| MP4 | 66 74 79 70 (Offset 4) | Davor steht ein Vier-Byte-Größenfeld |
Magic-Number-Validierung in Node.js implementieren
Das hier verwendete Paket file-type liegt in Version 22 vor und setzt Node.js
22 oder neuer voraus. Es erkennt Signaturen aus einem Buffer oder aus einer Datei. Die Bibliothek
ist ESM-only. Achten Sie daher darauf, dass die package.json Ihres Projekts
"type": "module" enthält, oder verwenden Sie Dateien mit der Endung
.mjs.
Installieren Sie die Abhängigkeiten für das Beispiel:
npm install file-type@22 express@4.22.2 multer@2.3.0
Eine Datei prüfen, ohne sie vollständig in den Speicher zu laden
import { fileTypeFromFile } from 'file-type'
/**
* Validate a file by magic number.
* @param {string} filePath Absolute or relative path to the file on disk
* @param {string[]} allowList Array of allowed MIME types
*/
export async function validateFileType(filePath, allowList = []) {
// Default allow-list: PNG, JPEG, and PDF
const allowedTypes = allowList.length ? allowList : ['image/png', 'image/jpeg', 'application/pdf']
const type = await fileTypeFromFile(filePath)
if (!type) throw new Error('Unknown or unsupported file type')
if (!allowedTypes.includes(type.mime)) {
throw new Error(`Disallowed file type: ${type.mime}`)
}
return { ...type, valid: true }
}
// Example
// (async () => {
// await validateFileType('uploads/avatar.png')
// })()
Größenbegrenzte Uploads mit Express.js validieren
Dieser Endpunkt puffert höchstens 5 MiB pro Datei und prüft anschließend deren Signatur. Der Upload wird bewusst nicht dauerhaft gespeichert. Es handelt sich nicht um eine Implementierung, die direkt auf die Festplatte streamt; begrenzen Sie bei der Speicherung im Arbeitsspeicher zusätzlich Parallelität und Anfrageraten.
import express from 'express'
import multer from 'multer'
import { fileTypeFromBuffer } from 'file-type'
const app = express()
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024, files: 1, fields: 0 },
})
app.post('/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) return res.status(400).json({ error: 'No file uploaded' })
const type = await fileTypeFromBuffer(req.file.buffer)
if (!type || !['image/png', 'image/jpeg'].includes(type.mime)) {
return res.status(400).json({ error: 'Invalid file type' })
}
res.json({ message: 'File validated', mime: type.mime, size: req.file.size })
} catch (err) {
res.status(400).json({ error: 'Unable to recognize this file' })
}
})
app.use((error, req, res, next) => {
if (res.headersSent) return next(error)
if (error instanceof multer.MulterError) {
return res.status(error.code === 'LIMIT_FILE_SIZE' ? 413 : 400).json({ error: 'Upload rejected' })
}
console.error('Upload processing failed')
res.status(500).json({ error: 'Unable to process the upload' })
})
app.listen(3000, () => console.log('API listening on :3000'))
Dateien in Python mit python-magic validieren
Das Beispiel setzt Python 3.10 oder neuer voraus. python-magic ist ein dünner
Wrapper um die C-Bibliothek libmagic, daher unterscheidet sich die
Installation je nach Plattform:
# Debian/Ubuntu
sudo apt-get install python3-magic libmagic1
# macOS (homebrew)
brew install libmagic
pip install python-magic
# Windows (pre-built binaries)
pip install python-magic-bin
from pathlib import Path
import magic
class FileValidator:
"""Validate MIME type using libmagic signatures."""
def __init__(self, allowed=None):
self.allowed = set(allowed or {
'image/png',
'image/jpeg',
'application/pdf',
})
self._mime = magic.Magic(mime=True)
def validate(self, file_path: str | Path) -> dict:
path = Path(file_path)
if not path.is_file():
raise FileNotFoundError(path)
mime_type = self._mime.from_file(str(path))
if mime_type not in self.allowed:
raise ValueError(f'Blocked MIME: {mime_type}')
return {
'mime': mime_type,
'size': path.stat().st_size,
'valid': True,
}
# Example
# validator = FileValidator()
# print(validator.validate('uploads/report.pdf'))
Sonderfälle und Polyglot-Dateien behandeln
Manche Dateien lassen sich als mehr als ein Format interpretieren. Die Suche nach kurzen Byte-Folgen
wie MZ, <html oder nach ZIP-Headern innerhalb
einer Datei ist kein zuverlässiger Polyglot-Detektor: Gewöhnliche Binärdaten können sie enthalten,
und gültige ZIP-basierte Dokumente enthalten wiederholt ZIP-Header. Signaturbibliotheken sind keine
Malware-Scanner. Lassen Sie auf die Identifizierung einen gepflegten, formatspezifischen Parser oder
Decoder in einem isolierten Prozess folgen, erzwingen Sie Ressourcenlimits und scannen oder
rekonstruieren Sie Inhalte entsprechend Ihrem Bedrohungsmodell. Führen Sie Uploads nicht aus und
liefern Sie keine aktiven Inhalte vom Ursprung Ihrer App aus.
Performance-Tipps für große Dateien
- Bevorzugen Sie die Datei- oder Stream-APIs der Bibliothek. Manche Formate benötigen mehr als ein Präfix fester Größe; eine universelle Erkennungsgarantie für 4.100 Byte gibt es nicht.
- Verarbeiten Sie Uploads als Streams, um zu vermeiden, dass ganze Dateien im Gigabyte-Bereich im Arbeitsspeicher gepuffert werden.
- Cachen Sie Arrays mit erlaubten Typen und reguläre Ausdrücke, insbesondere in Serverless-Umgebungen, in denen Cold Starts teuer sind.
Magic-Number-Validierung mit weiteren Schutzmaßnahmen kombinieren
Magic Numbers legen das Format einer Datei nahe; sie beweisen weder deren Gültigkeit noch deren Sicherheit. Stärken Sie Ihre Upload-Pipeline, indem Sie weitere Schutzebenen ergänzen:
- Erzwungene Byte-Limits für Uploads, auch bei Anfragen ohne vertrauenswürdigen Wert für
Content-Length - Viren-/Malware-Scans (ClamAV oder eine kommerzielle API)
- Rate Limiting und Authentifizierung an Upload-Endpunkten
- Content-Security-Policy-Header, wenn Sie von Nutzern bereitgestellte Medien ausliefern
Ihre Implementierung testen
Ein paar Unit-Tests (zum Beispiel mit Vitest) helfen sicherzustellen, dass künftige Refactorings die Validierung nicht beschädigen. Testen Sie mit gültigen Dateien, fehlerhaften Dateien und Sonderfällen wie leeren Dateien oder Dateien mit falscher Endung.
import { describe, expect, it } from 'vitest'
import { fileTypeFromBuffer } from 'file-type'
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+j3ioAAAAASUVORK5CYII=',
'base64',
)
const empty = Buffer.alloc(0)
const jpeg = Buffer.from(
'ffd8ffe000104a46494600010100000100010000ffdb0043000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101ffc00011080001000103012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffda000c03010002110311003f00f2a900',
'hex',
) // A JPEG-signature fixture; this test does not validate complete image decoding.
describe('magic-number validation', () => {
it('detects PNG correctly', async () => {
const t = await fileTypeFromBuffer(png)
expect(t?.mime).toBe('image/png')
})
it('detects JPEG correctly', async () => {
const t = await fileTypeFromBuffer(jpeg)
expect(t?.mime).toBe('image/jpeg')
})
it('rejects empty buffers', async () => {
const t = await fileTypeFromBuffer(empty)
expect(t).toBeUndefined()
})
})
Häufige Probleme beheben
| Symptom | Mögliche Ursache | Lösung |
|---|---|---|
Error: Unknown or unsupported file type | file-type findet keine passende Signatur | Übergeben Sie die vollständige Datei oder den vollständigen Buffer, prüfen Sie die unterstützten Formate und stellen Sie sicher, dass die Datei nicht verschlüsselt oder abgeschnitten ist |
Module not found: file-type | Es wird require() aus CommonJS verwendet | Wechseln Sie zu ESM oder nutzen Sie den dynamischen Import mit import('file-type') |
ImportError: failed to find libmagic | libmagic fehlt im Betriebssystem | Installieren Sie es mit dem Paketmanager oder verwenden Sie unter Windows das Wheel -bin |
Fazit
Prüfungen der Magic Numbers helfen, offensichtliche Typabweichungen vor der Speicherung oder Verarbeitung abzuweisen. Kombinieren Sie sie mit erzwungenen Größenlimits, formatbewusster Validierung, Malware-Scans und robuster Fehlerbehandlung; keine Signaturprüfung allein macht einen Upload sicher.
Sie möchten Uploads in großem Umfang einfacher handhaben? Sehen Sie sich unseren Service für Datei-Uploads bei Transloadit an.
