Skip to content

security: encode and sanitize values at frontend HTML sinks - #7970

Open
ar2rsawseen wants to merge 6 commits into
masterfrom
security/xss-sink-hardening
Open

security: encode and sanitize values at frontend HTML sinks#7970
ar2rsawseen wants to merge 6 commits into
masterfrom
security/xss-sink-hardening

Conversation

@ar2rsawseen

Copy link
Copy Markdown
Member

Consolidates the frontend HTML-sink hardening work into one PR. Each change routes an attacker-influenceable value through the right encoder/sanitizer at the point it is placed into an HTML sink, leaving normal display unchanged. Replaces #7966, #7968, #7961, #7954 and #7949.

  • core / graph note tooltip — the tooltip HTML-encodes the application name before rendering it (countly.common.js).
  • push / message editor — the composed message is sanitized before it is set as innerHTML on the editor's contenteditable, allowing only the user-property token span and its attributes (countly.views.component.common.js).
  • compliance-hub / export history — the export/purge history action cell HTML-encodes the application name; values that already come through the API escaper are left untouched, with a comment and a unit test (test/unit-tests/plugins.compliance-hub.actions-escaping.js) guarding both directions.
  • populator / confirm dialogs — dialog bodies render as text instead of HTML (environment_detail.html, populator.html).
  • core / res.expose script island< is escaped when serializing the exposed countlyGlobal object into the inline page script, and the active-app name is rendered with .text() instead of .html() (express-expose.js, countly.template.js).

Verified: node --check and eslint on all changed JS; the compliance-hub escaping unit test passes (5/5).

🤖 Generated with Claude Code

ar2rsawseen and others added 6 commits August 19, 2026 17:07
…oltip

The graph-note hover tooltip builds its content as an HTML string that includes the
application name from countlyGlobal, whose values are raw at runtime, and renders it via
tipsy html:true. Encode the application name with countlyCommon.encodeHtml so it renders
as text; the other values in the tooltip are API-encoded or i18n. Display is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…contenteditable

The push message editor set the composed message as innerHTML on a live
contenteditable. Sanitize that content with countlyCommon.encodeSomeHtml, allowing only
the user-property token span (and the attributes it relies on: class, id, contenteditable,
data-user-property-*) and escaping any other markup to inert text. The message body is
user text and the token element is the only legitimate markup, so display is unchanged for
normal messages; the token id is preserved so the editor's per-token event wiring keeps
working.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…istory actions column

The export/purge history datatable builds an html string in onReady and the
template renders it, so every value interpolated into it has to be html-safe
before it gets there.

Values taken from the row arrive through common.returnOutput, which
escape_html_entities has already escaped, so they are deliberately left alone:
escaping them a second time would surface the entities literally in the ui.
One value in that string comes from countlyGlobal instead. That object is
serialized into the dashboard by express-expose, whose escaping is for the
javascript string context and is value-preserving by design, so the api's html
escaping never applied to it. It is now escaped where it is interpolated.

Adds test/unit-tests/plugins.compliance-hub.actions-escaping.js, which loads
the real module in a sandbox and exercises the actual onReady builder. It pins
both directions: a value carrying markup is neutralized, and an already-escaped
api value is not double-escaped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ad of html

The populator template and environment delete confirmations rendered their
body with v-html, but the value bound there is a localized sentence with a name
substituted into a {0} placeholder. Both names are html-decoded on the way in,
so the escaping the api applied no longer held by the time they were rendered.

Text interpolation removes the sink instead of filtering what reaches it.
Checked every assignment to this dialog object and every populator locale file
that supplies these strings, in all 26 languages present: none contain tags, so
nothing renders differently.

Left the plugins plugin's dependency confirmation on v-html deliberately. Its
strings do carry markup: plugins.confirm has a <br/><br/> in all 24 translated
locale files, even though the default locale no longer has it. The values
interpolated into that one are plugin titles from package metadata rather than
anything a dashboard account can write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…S via app name)

The dashboard serialises the exposed countlyGlobal object into an inline
<script> block (dashboard.html: <script><%- javascript %></script>). The
serialiser only neutralised the exact sequence "</script>", but the HTML
tokeniser also ends a script element at "</script >", "</script/>" and other
whitespace/slash spellings, so an application name containing one of those
broke out of the script block. An app admin of a single app could store such
a name; any global admin who then loaded the dashboard (which lists every app)
executed the attacker's markup in their own session, escalating an app-admin
account to global-admin control.

Escape every "<" as < in both serialisation paths: string values (the
primitive branch, replacing the exact-match "</script>"/"<!--" replaces) and
object keys (escape_js_string). < parses back to "<", so every value read
from the exposed object is unchanged. This continues the value-preserving
direction of the previous fix (aa33b31, which removed the old HTML-entity
escape_html that corrupted values); it does not reintroduce entity escaping.

Also render the active-app name with .text() instead of .html() in
countly.template.js (line 2406), an independent DOM sink for the same value.

Verified: no raw "<" survives for any breakout spelling in values or keys; and
an eval round-trip of the serialiser output reproduces the input object
byte-for-byte and matches the previous serialiser's output (no dashboard-visible
change).

Reported through the security bug bounty programme (received 2026-08-17).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant