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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
150 lines
5.6 KiB
TypeScript
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)
|
|
})
|
|
})
|