Defense-in-depth for the font-family emit path in RichTextHtml (from #72). Not currently exploitable — the picker only offers the 7 hardcoded FontRegistry stacks, the whole declaration is wrapped in escapeAttr (a value can't break out of style="…" or inject a tag), and reply/forward reduces untrusted sender HTML to plaintext before it could reach RichStyle.FontFamily. This is insurance against those invariants changing.
Problem
RichTextHtml.inlineCss and baseCss interpolated the font-family value into the emitted style attribute raw. escapeAttr handles &/</>/", but not;/: — so a non-registry value could inject a sibling CSS declaration inside the attribute (e.g. Arial; color:red).
Fix
On emit, constrain the font-family value to a safe charset — the characters a real font stack uses (letters, digits, spaces, commas, quotes, hyphens, periods, underscores) — and drop anything else instead of emitting it raw. Applied in both inlineCss (span font) and baseCss (message-wide base style).
RichTextHtml is a pure module and must not depend on the UI-layer FontRegistry (which pulls in R.font.* / Compose), so the charset restriction is the layer-clean equivalent of the whitelist. All seven bundled FontRegistry stacks (sans-serif, serif, monospace, 'Inter', sans-serif, 'Lora', Georgia, serif, 'Merriweather', Georgia, serif, 'JetBrains Mono', monospace) are within the safe set, so the built-in fonts are unaffected.
Test plan
:app:assembleDebug
:app:testDebugUnitTest — new RichTextHtmlTest cases: an unsafe inline font-family (Arial; color:red) is dropped (no color:red, no font-family) while the run's other styling (<b>) survives; an unsafe base-style fontCss is dropped while the wrapper's safe font-size:12pt still emits; a registry stack with quotes/commas ('Inter', sans-serif) still emits its font-family. Existing font round-trip tests still pass.
## Summary
Defense-in-depth for the font-family emit path in `RichTextHtml` (from #72). **Not currently exploitable** — the picker only offers the 7 hardcoded `FontRegistry` stacks, the whole declaration is wrapped in `escapeAttr` (a value can't break out of `style="…"` or inject a tag), and reply/forward reduces untrusted sender HTML to plaintext before it could reach `RichStyle.FontFamily`. This is insurance against those invariants changing.
## Problem
`RichTextHtml.inlineCss` and `baseCss` interpolated the `font-family` value into the emitted `style` attribute raw. `escapeAttr` handles `&`/`<`/`>`/`"`, but **not** `;`/`:` — so a non-registry value could inject a sibling CSS declaration inside the attribute (e.g. `Arial; color:red`).
## Fix
On emit, constrain the `font-family` value to a **safe charset** — the characters a real font stack uses (letters, digits, spaces, commas, quotes, hyphens, periods, underscores) — and **drop** anything else instead of emitting it raw. Applied in both `inlineCss` (span font) and `baseCss` (message-wide base style).
`RichTextHtml` is a pure module and must not depend on the UI-layer `FontRegistry` (which pulls in `R.font.*` / Compose), so the charset restriction is the layer-clean equivalent of the whitelist. All seven bundled `FontRegistry` stacks (`sans-serif`, `serif`, `monospace`, `'Inter', sans-serif`, `'Lora', Georgia, serif`, `'Merriweather', Georgia, serif`, `'JetBrains Mono', monospace`) are within the safe set, so **the built-in fonts are unaffected**.
## Test plan
- [x] `:app:assembleDebug`
- [x] `:app:testDebugUnitTest` — new `RichTextHtmlTest` cases: an unsafe inline `font-family` (`Arial; color:red`) is dropped (no `color:red`, no `font-family`) while the run's other styling (`<b>`) survives; an unsafe base-style `fontCss` is dropped while the wrapper's safe `font-size:12pt` still emits; a registry stack with quotes/commas (`'Inter', sans-serif`) still emits its `font-family`. Existing font round-trip tests still pass.
- [x] `:app:lintDebug`
- [x] `:app:ktlintCheck :app:detekt`
- [x] `:app:compileDebugAndroidTestKotlin`
Closes #205
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Summary
Defense-in-depth for the font-family emit path in
RichTextHtml(from #72). Not currently exploitable — the picker only offers the 7 hardcodedFontRegistrystacks, the whole declaration is wrapped inescapeAttr(a value can't break out ofstyle="…"or inject a tag), and reply/forward reduces untrusted sender HTML to plaintext before it could reachRichStyle.FontFamily. This is insurance against those invariants changing.Problem
RichTextHtml.inlineCssandbaseCssinterpolated thefont-familyvalue into the emittedstyleattribute raw.escapeAttrhandles&/</>/", but not;/:— so a non-registry value could inject a sibling CSS declaration inside the attribute (e.g.Arial; color:red).Fix
On emit, constrain the
font-familyvalue to a safe charset — the characters a real font stack uses (letters, digits, spaces, commas, quotes, hyphens, periods, underscores) — and drop anything else instead of emitting it raw. Applied in bothinlineCss(span font) andbaseCss(message-wide base style).RichTextHtmlis a pure module and must not depend on the UI-layerFontRegistry(which pulls inR.font.*/ Compose), so the charset restriction is the layer-clean equivalent of the whitelist. All seven bundledFontRegistrystacks (sans-serif,serif,monospace,'Inter', sans-serif,'Lora', Georgia, serif,'Merriweather', Georgia, serif,'JetBrains Mono', monospace) are within the safe set, so the built-in fonts are unaffected.Test plan
:app:assembleDebug:app:testDebugUnitTest— newRichTextHtmlTestcases: an unsafe inlinefont-family(Arial; color:red) is dropped (nocolor:red, nofont-family) while the run's other styling (<b>) survives; an unsafe base-stylefontCssis dropped while the wrapper's safefont-size:12ptstill emits; a registry stack with quotes/commas ('Inter', sans-serif) still emits itsfont-family. Existing font round-trip tests still pass.:app:lintDebug:app:ktlintCheck :app:detekt:app:compileDebugAndroidTestKotlinCloses #205
🤖 Generated with Claude Code