Sanitising a message's HTML still asks for its remote fonts and stylesheets #142

Closed
opened 2026-09-07 18:07:44 -04:00 by Salastil · 1 comment
Owner

Found while tracing #141, and unrelated to it.

The problem

Watching for CSP violations while switching between conversations turns up two
that fire regularly and are nothing to do with moho's own code:

style-src-elem  https://cdn.prod.website-files.com/.../discord-2022.shared.css
font-src        https://assets.kick.com/main/_next/static/media/*.woff2

Both are remote subresources named by remote message HTML. RichText sanitises
a formatted body by parsing it with DOMParser, whose document is inert - "no
scripts run, no images load", as the comment there says - but Chromium still
attempts stylesheet and font loads out of that parse, and it is the window's
Content-Security-Policy that stops them.

Nothing is drawn from them: the sanitiser keeps an allowlist of elements and
these are not on it. But the requests are attempted, which means the policy is
currently the only thing standing between reading a message and that message's
author learning when it was read. That is a load-bearing role nothing documents
it as having, and a policy is a backstop rather than a design.

The solution

Strip the elements that name remote subresources - link, style, base,
and anything else that can pull - before the parsed document has a chance to
attempt them, or parse into a context that cannot fetch at all (an inert
document created with document.implementation.createHTMLDocument() behaves
differently here and is worth measuring against DOMParser).

Then keep the policy exactly as it is, and add a note to richtext.tsx saying
what it is holding up, so the next person to widen it knows what they are
widening.

Worth a test: a formatted body containing <link rel="stylesheet"> and
@font-face should produce no violation at all, rather than a blocked one.

Found while tracing #141, and unrelated to it. ## The problem Watching for CSP violations while switching between conversations turns up two that fire regularly and are nothing to do with moho's own code: style-src-elem https://cdn.prod.website-files.com/.../discord-2022.shared.css font-src https://assets.kick.com/main/_next/static/media/*.woff2 Both are remote subresources named by remote message HTML. `RichText` sanitises a formatted body by parsing it with `DOMParser`, whose document is inert - "no scripts run, no images load", as the comment there says - but Chromium still attempts stylesheet and font loads out of that parse, and it is the window's Content-Security-Policy that stops them. Nothing is drawn from them: the sanitiser keeps an allowlist of elements and these are not on it. But the requests are attempted, which means the policy is currently the only thing standing between reading a message and that message's author learning when it was read. That is a load-bearing role nothing documents it as having, and a policy is a backstop rather than a design. ## The solution Strip the elements that name remote subresources - `link`, `style`, `base`, and anything else that can pull - before the parsed document has a chance to attempt them, or parse into a context that cannot fetch at all (an inert document created with `document.implementation.createHTMLDocument()` behaves differently here and is worth measuring against `DOMParser`). Then keep the policy exactly as it is, and add a note to `richtext.tsx` saying what it is holding up, so the next person to widen it knows what they are widening. Worth a test: a formatted body containing `<link rel="stylesheet">` and `@font-face` should produce no violation at all, rather than a blocked one.
Salastil added the buglow priority labels 2026-09-07 18:07:44 -04:00
Author
Owner

Not fixed - disproven. Closing with what was measured, because the next
person to see those blocked requests should start from here rather than from
this issue's guess.

The sanitiser does not fetch anything. A body carrying every shape this
issue accused it of was parsed through the same DOMParser path in the
running client, with the network watched:

<link rel="stylesheet" href="https://cdn.example.com/probe-css.css">
<style>@font-face{font-family:p;src:url(https://cdn.example.com/probe-font.woff2)}</style>
<img src="https://cdn.example.com/probe-img.png">
<div style="background:url(https://cdn.example.com/probe-bg.png)">x</div>
<script src="https://cdn.example.com/probe-js.js"></script>

  network requests for probe assets: none
  CSP violations: []

The document is inert exactly as its comment claims. Note that CSP violations
alone were not a sufficient detector here - img-src allows https:, so an
image fetch would have succeeded silently; watching the network is what
settles it.

And it could not have been the sanitiser anyway. <link> and <style>
are not in ALLOWED_TAGS, DROP_CONTENT discards their text, and style
attributes only pass colour, weight, font-style and text-decoration through
strict patterns - there is no url() that survives.

Nor is it in the data. Of every message currently stored - body, formatted
html, embeds and components - none contains website-files, assets.kick.com,
<link, @font-face or <style.

And it does not reproduce. A full reload with a violation listener armed
for every document, followed by a walk through all fifteen accounts and
guilds: zero.

The original observation was real - captured twice on 2026-09-08 - so
something did request them, and I cannot say what. If they reappear, the
useful next step is to capture documentURI on the violation event, which
names the document that asked; everything above is already ruled out.

What did come out of this is #145: the sanitiser passes any https image
through to a rendered <img>, which is how custom emoji work and also how a
sender can learn their message was read. That one is real, is a design
decision rather than a bug, and is written up there.

Comment recorded in richtext.tsx so the claim is not re-derived: adc83b8

Not fixed - disproven. Closing with what was measured, because the next person to see those blocked requests should start from here rather than from this issue's guess. **The sanitiser does not fetch anything.** A body carrying every shape this issue accused it of was parsed through the same `DOMParser` path in the running client, with the network watched: ``` <link rel="stylesheet" href="https://cdn.example.com/probe-css.css"> <style>@font-face{font-family:p;src:url(https://cdn.example.com/probe-font.woff2)}</style> <img src="https://cdn.example.com/probe-img.png"> <div style="background:url(https://cdn.example.com/probe-bg.png)">x</div> <script src="https://cdn.example.com/probe-js.js"></script> network requests for probe assets: none CSP violations: [] ``` The document is inert exactly as its comment claims. Note that CSP violations alone were not a sufficient detector here - `img-src` allows `https:`, so an image fetch would have succeeded silently; watching the network is what settles it. **And it could not have been the sanitiser anyway.** `<link>` and `<style>` are not in `ALLOWED_TAGS`, `DROP_CONTENT` discards their text, and style attributes only pass colour, weight, font-style and text-decoration through strict patterns - there is no `url()` that survives. **Nor is it in the data.** Of every message currently stored - body, formatted html, embeds and components - none contains `website-files`, `assets.kick.com`, `<link`, `@font-face` or `<style`. **And it does not reproduce.** A full reload with a violation listener armed for every document, followed by a walk through all fifteen accounts and guilds: zero. The original observation was real - captured twice on 2026-09-08 - so something did request them, and I cannot say what. If they reappear, the useful next step is to capture `documentURI` on the violation event, which names the document that asked; everything above is already ruled out. What did come out of this is #145: the sanitiser passes any https image through to a rendered `<img>`, which is how custom emoji work and also how a sender can learn their message was read. That one is real, is a design decision rather than a bug, and is written up there. Comment recorded in `richtext.tsx` so the claim is not re-derived: adc83b8
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#142