fix(notes): en note kunne smugle et script ind gennem et billedes alt-tekst

Indført i aften, sammen med billedvisningen, og fundet ved at læse den igen.

`escapeHtml` tager &, < og > — ikke anførselstegn, og det er rigtigt for tekst
mellem to tags, hvor et anførselstegn ikke betyder noget. `alt` er ikke tekst
mellem to tags. Det står inde i en attribut, og der lukker det første
anførselstegn den, hvorefter resten står som markup:

    !["  onerror="alert(1)](/api/v1/attachments/aaaa)

blev til et <img> med en onerror på. Adressen var i forvejen bundet til vores
eget bilagsmønster, så den var ikke vejen ind — teksten ved siden af var.

Det er ikke en teoretisk vej. En note deles ved at ligge i et projekt, så den
læses af alle, der kan læse projektet, og scriptet ville køre på vores eget
domæne med sessionscookien, hos den der åbnede noten.

En værdi i en attribut har brug for sin egen undslipning, og har nu en. Prøven
skriver noten, åbner den og ser efter, at hændelsen står som tekst i alt og ikke
som en attribut på elementet — og at billedets 404, som er selve vejen en
onerror ville blive kaldt ad, ikke kalder noget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-20 06:39:19 +02:00
co-authored by Claude Opus 5
parent dee2d5e30b
commit 052f8a43fb
3 changed files with 58 additions and 1 deletions
+19 -1
View File
@@ -37,6 +37,16 @@ const BLOCKS = [
const escapeHtml = (s) =>
s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
/**
* En værdi, der skal stå inde i en attribut.
*
* escapeHtml er skrevet til tekst mellem to tags, hvor et anførselstegn ikke
* betyder noget, og lader dem derfor stå. Inde i en attribut lukker de den — og
* så står resten af strengen som markup. Det er den ene forskel, og den er hele
* forskellen mellem tekst og et onerror.
*/
const attr = (s) => escapeHtml(s).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
/** Inline marks, innermost first so a bold word inside italics survives both. */
function inlineToHtml(text) {
return (
@@ -51,8 +61,16 @@ function inlineToHtml(text) {
// ellers hente fra en fremmed vært, når noten åbnes, og fortælle den, at
// den er blevet læst — og en note er det sidste sted, man vil have en
// sporingspixel. Alt andet står som den tekst, det er.
//
// `alt` skal citat-undslippes her, og det er ikke en formalitet.
// escapeHtml tager &, < og > — ikke anførselstegn, fordi den er skrevet
// til tekst mellem to tags, hvor et anførselstegn ikke betyder noget. Her
// står værdien inde i en attribut, og der lukker et anførselstegn den:
// `![" onerror="alert(1)](/api/v1/attachments/aaaa)` blev til et img med
// en onerror på. En note er delt gennem et projekt, så det ville køre på
// vores eget domæne med sessionscookien, hos den, der åbnede noten.
.replace(/!\[([^\]]*)\]\((\/api\/v1\/attachments\/[0-9a-f-]+)\)/g,
(_, alt, src) => `<img src="${src}" alt="${alt}">`)
(_, alt, src) => `<img src="${src}" alt="${attr(alt)}">`)
.replace(/&lt;u&gt;(.+?)&lt;\/u&gt;/g, '<u>$1</u>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')