Files
AfiS-Aktensystem/web/tests/dateitypen.test.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

160 lines
6.7 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import { ALLOWED_TYPES, detectType, looksLikeText, matchesDeclared } from '@/lib/files/magic'
import { absolutePath, storageKeyFor } from '@/lib/files/paths'
import { deliveryFileName } from '@/lib/files/delivery'
/**
* Die Typprüfung ist die Stelle, an der eine feindselige Datei auffallen muss.
*
* Geprüft wird deshalb nicht nur, dass die erlaubten Formate durchkommen,
* sondern vor allem, dass die verbotenen es **nicht** tun - und zwar auch
* dann, wenn sie sich als erlaubte ausgeben.
*/
const bytes = (...werte: number[]) => new Uint8Array(werte)
const text = (wert: string) => new TextEncoder().encode(wert)
const PNG = bytes(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13)
const JPEG = bytes(0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46)
const WEBP = new Uint8Array([...text('RIFF'), 0, 0, 0, 0, ...text('WEBP'), ...text('VP8 ')])
const PDF = text('%PDF-1.7\n1 0 obj\n')
describe('Erkennung erlaubter Typen', () => {
it('erkennt jedes zugelassene Format am Inhalt', () => {
expect(detectType(PNG)).toEqual({ ok: true, type: 'image/png' })
expect(detectType(JPEG)).toEqual({ ok: true, type: 'image/jpeg' })
expect(detectType(WEBP)).toEqual({ ok: true, type: 'image/webp' })
expect(detectType(PDF)).toEqual({ ok: true, type: 'application/pdf' })
expect(detectType(text('Nur Text, mehr nicht.\n'))).toEqual({
ok: true,
type: 'text/plain',
})
})
it('deckt genau die fünf Typen aus dem Dokument ab', () => {
expect([...ALLOWED_TYPES]).toEqual([
'image/png',
'image/jpeg',
'image/webp',
'application/pdf',
'text/plain',
])
})
})
describe('Abwehr', () => {
it('lehnt SVG ab, auch wenn es wie Text aussieht', () => {
// Der eigentliche Grund für die ganze Kette: SVG führt im Browser Skripte
// aus. Es ist gültiges UTF-8 und käme sonst als text/plain durch - das ist
// in Ordnung, denn als Text wird es nie gerendert, sondern ausgeliefert
// mit Content-Disposition: attachment.
const svg = text('<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>')
const erkannt = detectType(svg)
expect(erkannt).toEqual({ ok: true, type: 'text/plain' })
// Entscheidend: es ist nie image/svg+xml, und der gemeldete Typ passt nicht.
expect(matchesDeclared('text/plain', 'image/svg+xml')).toBe(false)
})
it('lehnt Archive und Ausführbares ab', () => {
expect(detectType(bytes(0x50, 0x4b, 0x03, 0x04, 0, 0))).toEqual({
ok: false,
reason: 'unknown-type',
})
// ELF
expect(detectType(bytes(0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0))).toEqual({
ok: false,
reason: 'unknown-type',
})
// Windows-Programm
expect(detectType(bytes(0x4d, 0x5a, 0x90, 0x00))).toEqual({
ok: false,
reason: 'unknown-type',
})
})
it('lässt sich vom RIFF-Container nicht täuschen', () => {
// WAV ist auch RIFF. Ohne die zweite Prüfung wäre es ein WebP.
const wav = new Uint8Array([...text('RIFF'), 0, 0, 0, 0, ...text('WAVE')])
expect(detectType(wav)).toEqual({ ok: false, reason: 'unknown-type' })
})
it('erkennt ein PDF mit Vorspann nicht als PDF', () => {
// Müll vor %PDF- ist der klassische Weg, eine Datei für zwei Programme
// zugleich lesbar zu machen. Die Spezifikation erlaubt es, wir nicht: die
// Signatur muss an Position 0 stehen.
const getarnt = new Uint8Array([...text('GIF89a'), ...PDF])
const erkannt = detectType(getarnt)
expect(erkannt).not.toEqual({ ok: true, type: 'application/pdf' })
// Es bleibt lesbarer Text und wird deshalb als solcher angenommen. Das ist
// harmlos, weil die Auslieferung die Endung auf den **erkannten** Typ
// setzt: die Datei kommt als .txt an und landet in keinem PDF-Betrachter.
expect(erkannt).toEqual({ ok: true, type: 'text/plain' })
expect(deliveryFileName('bericht.pdf', 'text/plain')).toBe('bericht.txt')
})
it('lehnt einen Vorspann mit Binärdaten ganz ab', () => {
const getarnt = new Uint8Array([0x00, 0x01, ...text('%PDF-1.7')])
expect(detectType(getarnt)).toEqual({ ok: false, reason: 'unknown-type' })
})
it('lässt kein Binärformat als Text durch', () => {
expect(looksLikeText(bytes(0x48, 0x00, 0x69))).toBe(false)
expect(looksLikeText(bytes(0xff, 0xfe, 0x41))).toBe(false)
expect(looksLikeText(text('Umlaute sind erlaubt: äöüß\n\t'))).toBe(true)
})
it('lehnt eine leere Datei ab', () => {
expect(detectType(new Uint8Array())).toEqual({ ok: false, reason: 'empty' })
})
it('erkennt, wenn der gemeldete Typ nicht zum Inhalt passt', () => {
expect(matchesDeclared('image/png', 'application/pdf')).toBe(false)
expect(matchesDeclared('image/png', 'image/png')).toBe(true)
// Der Browser weiss es manchmal selbst nicht; das ist kein Verdacht.
expect(matchesDeclared('image/png', 'application/octet-stream')).toBe(true)
expect(matchesDeclared('image/png', null)).toBe(true)
// Zeichensatzangabe im Kopf stört nicht.
expect(matchesDeclared('text/plain', 'text/plain; charset=utf-8')).toBe(true)
})
})
describe('Ablagepfade', () => {
it('verteilt auf zwei Ebenen und behält die Kennung', () => {
expect(storageKeyFor('store', '0192abcd-ef01-7000-8000-000000000001')).toBe(
'store/01/92/0192abcd-ef01-7000-8000-000000000001',
)
})
it('lässt keinen Pfad aus dem Dateiverzeichnis heraus', () => {
process.env.FILES_DIR ??= '/tmp/afis-test-files'
// Kommt aus der Datenbank, nicht aus einer Eingabe - aber ein
// Verzeichniswechsel wäre der teuerste Fehler an dieser Stelle.
expect(() => absolutePath('../../etc/passwd')).toThrow()
expect(() => absolutePath('store/../../../etc/passwd')).toThrow()
expect(absolutePath('store/ab/cd/ef')).toContain('store/ab/cd/ef')
})
it('wirft Sonderzeichen aus der Kennung, bevor sie ein Pfad wird', () => {
expect(storageKeyFor('quarantine', '../../etc')).not.toContain('..')
})
})
describe('Name beim Ausliefern', () => {
it('setzt die Endung auf den erkannten Typ, nicht auf den hochgeladenen Namen', () => {
// Sonst trüge eine als Text erkannte Datei weiter .pdf und würde beim
// Doppelklick im PDF-Betrachter geöffnet.
expect(deliveryFileName('urlaub.jpeg', 'image/png')).toBe('urlaub.png')
expect(deliveryFileName('notiz', 'text/plain')).toBe('notiz.txt')
expect(deliveryFileName('vertrag.pdf', 'application/pdf')).toBe('vertrag.pdf')
expect(deliveryFileName('bild.JPG', 'image/jpeg')).toBe('bild.jpg')
})
it('lässt keine Kopfzeile zerlegen', () => {
const boes = deliveryFileName('a"; rm -rf /\r\nX: y.txt', 'text/plain')
expect(boes).not.toContain('"')
expect(boes).not.toMatch(/[\r\n]/)
})
})