Files
AfiS-Aktensystem/web/tests/einschleusen.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

150 lines
5.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { templateFieldsSchema } from '@/lib/templates/schema'
import { fillPlaceholders } from '@/lib/templates/fields'
import { colorVariable, COLOR_TOKENS } from '@/components/StampMark'
import { deliveryFileName } from '@/lib/files/delivery'
import { splitBody } from '@/lib/chat/mentions'
/**
* Was passiert, wenn jemand versucht, über ein Textfeld Code einzuschleusen.
*
* Die Grundlage ist, dass React alles maskiert, was es als Text rendert, und
* dass es in dieser Anwendung genau **ein** `dangerouslySetInnerHTML` gibt -
* mit einer Konstanten aus demselben Modul. Diese Datei prüft die Ränder darum
* herum: die Stellen, an denen Text nicht als Text ankommt, sondern als
* Vorlage, als Stilangabe oder als Kopfzeile.
*/
const BOESARTIG = [
'<script>alert(1)</script>',
'"><img src=x onerror=alert(1)>',
'javascript:alert(1)',
'{{constructor.constructor("alert(1)")()}}',
]
describe('Vorlagen nehmen nur an, was sie kennen', () => {
it('weist unbekannte Schlüssel ab, statt sie still zu verwerfen', () => {
// Still verwerfen wäre die schlechtere Antwort: die Vorlage wäre
// gespeichert und täte nicht, was dasteht.
const ergebnis = templateFieldsSchema.safeParse([
{ key: 'titel', label: 'Titel', type: 'shorttext', onClick: 'alert(1)' },
])
expect(ergebnis.success).toBe(false)
})
it('nimmt keine erfundene Feldart', () => {
for (const art of ['script', 'html', 'iframe', '__proto__']) {
const ergebnis = templateFieldsSchema.safeParse([
{ key: 'feld', label: 'Feld', type: art },
])
expect(ergebnis.success, art).toBe(false)
}
})
it('nimmt keinen Schlüssel, der kein Bezeichner ist', () => {
for (const schluessel of ['__proto__', 'a-b', 'A', '../x', '']) {
const ergebnis = templateFieldsSchema.safeParse([
{ key: schluessel, label: 'Feld', type: 'shorttext' },
])
expect(ergebnis.success, schluessel).toBe(false)
}
})
it('lässt bösartigen Text als Beschriftung durch - als Text', () => {
// Das ist Absicht: eine Beschriftung darf jedes Zeichen enthalten, weil sie
// als Text gerendert wird und nie als HTML. Verboten ist die Struktur,
// nicht das Zeichen.
for (const text of BOESARTIG) {
const ergebnis = templateFieldsSchema.safeParse([
{ key: 'feld', label: text, type: 'shorttext' },
])
expect(ergebnis.success, text).toBe(true)
}
})
})
describe('Platzhalter bleiben Text', () => {
it('setzt Werte ein, ohne sie auszuwerten', () => {
const text = fillPlaceholders('Hallo {{name}}', {
name: '<script>alert(1)</script>',
})
// Unverändert übernommen - und React macht daraus beim Rendern Text.
expect(text).toBe('Hallo <script>alert(1)</script>')
})
it('holt nichts aus der Prototypkette', () => {
// `constructor` ist ein gültiger Feldschlüssel: die Schlüsselregel erlaubt
// Kleinbuchstaben, und mehr braucht das Wort nicht. Ohne die Prüfung auf
// eigene Eigenschaften stand danach der Quelltext der geerbten Funktion im
// Dokument.
expect(fillPlaceholders('{{constructor}}', { anderes: 'x' })).toBe('[constructor]')
// Als echtes Feld funktioniert derselbe Schlüssel weiterhin.
expect(fillPlaceholders('{{constructor}}', { constructor: 'Bauleitung' })).toBe(
'Bauleitung',
)
})
it('erkennt nur Kleinbuchstaben als Platzhalter', () => {
// Zweite Schranke vor derselben Gefahr: `toString` und `valueOf` tragen
// Grossbuchstaben und sind damit schon syntaktisch kein Platzhalter. Sie
// bleiben stehen, wie sie dastehen.
for (const name of ['toString', 'valueOf', 'hasOwnProperty', '__proto__']) {
expect(fillPlaceholders(`{{${name}}}`, { anderes: 'x' })).toBe(`{{${name}}}`)
}
})
it('führt keinen Ausdruck aus, der wie einer aussieht', () => {
const text = fillPlaceholders('{{a}}', { a: '{{b}}', b: 'nie' })
// Kein zweiter Durchlauf: eingesetzte Werte werden nicht erneut ersetzt.
expect(text).toBe('{{b}}')
})
})
describe('Farbmarken aus der Datenbank', () => {
it('lässt nur bekannte Marken in ein Stilattribut', () => {
for (const token of COLOR_TOKENS) {
expect(colorVariable(token)).toBe(`var(--color-${token})`)
}
})
it('fällt bei allem anderen auf die neutrale Marke zurück', () => {
for (const versuch of [
'stamp-0); background: url(https://example.invalid/x',
'x); position:fixed; inset:0',
'',
null,
undefined,
]) {
expect(colorVariable(versuch as string)).toBe('var(--color-stamp-0)')
}
})
})
describe('Dateinamen in der Kopfzeile', () => {
it('lässt keinen Zeilenumbruch und kein Anführungszeichen durch', () => {
const name = deliveryFileName('a"\r\nX-Injected: 1\\.png', 'image/png')
expect(name).not.toMatch(/["\r\n\\]/)
expect(name.endsWith('.png')).toBe(true)
})
it('erzwingt die Endung aus dem geprüften Typ, nicht aus dem Namen', () => {
// Der Name kommt vom Hochladenden, der Typ aus den Magic Bytes.
expect(deliveryFileName('harmlos.png.html', 'application/pdf')).toBe('harmlos.png.pdf')
})
})
describe('Nachrichtentext wird zerlegt, nicht gebaut', () => {
it('macht aus Auszeichnung keine Struktur', () => {
const stuecke = splitBody('<script>alert(1)</script> @jemand AfiS-2026-0001')
// Der gefährliche Teil bleibt ein Textstück; nur Erwähnung und Aktenzeichen
// werden als eigene Art erkannt.
const text = stuecke
.filter((s) => s.kind === 'text')
.map((s) => s.value)
.join('')
expect(text).toContain('<script>alert(1)</script>')
expect(stuecke.some((s) => s.kind === 'mention')).toBe(true)
})
})