Bug description
decodeXmlEntities in the feed digest parser decodes & first:
https://github.com/koala73/worldmonitor/blob/main/server/worldmonitor/news/v1/list-feed-digest.ts#L751-L760
function decodeXmlEntities(s: string): string {
return s
.replace(/&/g, '&') // <-- must be last
.replace(/</g, '<')
.replace(/>/g, '>')
...
}
& has to be decoded last. Decoding it first turns the escaped ampersand of &lt; into a live &, which the very next .replace immediately consumes as < → <. One pass decodes two levels.
The visible damage differs by call site:
extractTag (:748, used for <title> among others): a headline whose literal text is <script> arrives escaped as &lt;script&gt; and comes back as <script> — raw markup injected into ParsedItem.title and emitted on the wire instead of the text the publisher wrote.
extractDescription (:705): it strips tags after decoding (.replace(/<[^>]+>/g, ' ')), so the same input loses the words entirely.
Separately, the numeric-reference branches use String.fromCharCode, which truncates to 16 bits — 😀 decodes to U+F600 (a private-use glyph) rather than 😀.
Steps to reproduce
Feed item:
<item>
<title>XSS via &lt;script&gt; in Acme SDK</title>
<description>Acme patched an XSS triggered by &lt;script&gt; tags in user-supplied profile bios.</description>
</item>
Observed:
| input |
expected |
actual |
| title |
XSS via <script> in Acme SDK |
XSS via <script> in Acme SDK |
| description |
...triggered by <script> tags in user-supplied profile bios. |
...triggered by tags in user-supplied profile bios. |
😀 |
😀 |
U+F600 |
� |
dropped |
짿 |
The description case is a silent content deletion — the escaped markup and the surrounding words disappear from the digest.
Expected behavior
One decode pass should decode exactly one level of escaping: escaping a plain string and then decoding it should return the original string.
Notes
Happy to send a PR moving the & replace to the end and switching the numeric branches to String.fromCodePoint (guarding the out-of-range case so one malformed reference can't throw RangeError and fail the whole feed parse). I have a round-trip regression test ready — escape a plain string, decode it, assert the original comes back — which fails on 5 of 6 cases against the current implementation.
Affected area: News panels / RSS feeds — server/worldmonitor/news/v1/list-feed-digest.ts. Not variant-specific.
Bug description
decodeXmlEntitiesin the feed digest parser decodes&first:https://github.com/koala73/worldmonitor/blob/main/server/worldmonitor/news/v1/list-feed-digest.ts#L751-L760
&has to be decoded last. Decoding it first turns the escaped ampersand of&lt;into a live&, which the very next.replaceimmediately consumes as<→<. One pass decodes two levels.The visible damage differs by call site:
extractTag(:748, used for<title>among others): a headline whose literal text is<script>arrives escaped as&lt;script&gt;and comes back as<script>— raw markup injected intoParsedItem.titleand emitted on the wire instead of the text the publisher wrote.extractDescription(:705): it strips tags after decoding (.replace(/<[^>]+>/g, ' ')), so the same input loses the words entirely.Separately, the numeric-reference branches use
String.fromCharCode, which truncates to 16 bits —😀decodes to U+F600 (a private-use glyph) rather than 😀.Steps to reproduce
Feed item:
Observed:
XSS via <script> in Acme SDKXSS via <script> in Acme SDK...triggered by <script> tags in user-supplied profile bios....triggered by tags in user-supplied profile bios.😀�짿The description case is a silent content deletion — the escaped markup and the surrounding words disappear from the digest.
Expected behavior
One decode pass should decode exactly one level of escaping: escaping a plain string and then decoding it should return the original string.
Notes
Happy to send a PR moving the
&replace to the end and switching the numeric branches toString.fromCodePoint(guarding the out-of-range case so one malformed reference can't throwRangeErrorand fail the whole feed parse). I have a round-trip regression test ready — escape a plain string, decode it, assert the original comes back — which fails on 5 of 6 cases against the current implementation.Affected area: News panels / RSS feeds —
server/worldmonitor/news/v1/list-feed-digest.ts. Not variant-specific.