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.
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:
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
Both are remote subresources named by remote message HTML.
RichTextsanitisesa formatted body by parsing it with
DOMParser, whose document is inert - "noscripts 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()behavesdifferently here and is worth measuring against
DOMParser).Then keep the policy exactly as it is, and add a note to
richtext.tsxsayingwhat 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-faceshould produce no violation at all, rather than a blocked one.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
DOMParserpath in therunning client, with the network watched:
The document is inert exactly as its comment claims. Note that CSP violations
alone were not a sufficient detector here -
img-srcallowshttps:, so animage 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_CONTENTdiscards their text, and styleattributes 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-faceor<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
documentURIon the violation event, whichnames 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 asender 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.tsxso the claim is not re-derived:adc83b8