Skip to content

decodeXmlEntities decodes & first, double-decoding RSS titles and silently deleting text from descriptions #5428

Description

@thejesh23

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(/&amp;/g, '&')      // <-- must be last
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    ...
}

&amp; has to be decoded last. Decoding it first turns the escaped ampersand of &amp;lt; into a live &, which the very next .replace immediately consumes as &lt;<. 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 &lt;script&gt; arrives escaped as &amp;lt;script&amp;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 — &#128512; decodes to U+F600 (a private-use glyph) rather than 😀.

Steps to reproduce

Feed item:

<item>
  <title>XSS via &amp;lt;script&amp;gt; in Acme SDK</title>
  <description>Acme patched an XSS triggered by &amp;lt;script&amp;gt; tags in user-supplied profile bios.</description>
</item>

Observed:

input expected actual
title XSS via &lt;script&gt; in Acme SDK XSS via <script> in Acme SDK
description ...triggered by &lt;script&gt; tags in user-supplied profile bios. ...triggered by tags in user-supplied profile bios.
&#128512; 😀 U+F600
&#999999999; 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 &amp; 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions