[security][core] escape values at dashboard html sinks the api escaper does not cover - #7954
Open
ar2rsawseen wants to merge 2 commits into
Open
[security][core] escape values at dashboard html sinks the api escaper does not cover#7954ar2rsawseen wants to merge 2 commits into
ar2rsawseen wants to merge 2 commits into
Conversation
…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>
Three confirm dialogs rendered their body with v-html, but the value bound
there is always a localized sentence with a name substituted into a {0}
placeholder, so it is never markup. Checked every assignment to these dialog
objects and every locale file that supplies the strings: none of them contain
tags, in any language. Text interpolation is what these were always meant to
be, and it removes the sink rather than filtering what reaches it.
Two of the three were reachable with a value that had been html-decoded on the
way in, so the escaping the api applied no longer held by the time it was
rendered.
- populator: template and environment delete confirmations
- plugins: the enable/disable dependency confirmation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while looking at how the dashboard's HTML sinks are fed, alongside #7949.
Countly's model is that API output is HTML-escaped server side by
common.escape_html_entities, so a value that came from an API response is already safe by the time a template renders it. This PR covers the two situations where that guarantee does not hold, and were the only ones the sweep found still open.1. A value the API escaper does not cover
The compliance-hub export/purge history datatable builds an HTML string in
onReadyand the template renders it. Most of what goes into that string comes from the row, i.e. throughcommon.returnOutput, and is already escaped: those are deliberately left alone, because escaping them a second time would surface the entities literally in the UI.One value in that string comes from
countlyGlobalinstead. That object is serialized into the dashboard byexpress-expose, and that serializer escapes for the JavaScript string context, not the HTML one. It is value-preserving by design (see #7949), so the API's HTML escaping was never applied to this value at any point. It is now escaped where it is interpolated.2. Dialog bodies that never needed HTML
Three confirm dialogs rendered their body with
v-html, where the bound value is always a localized sentence with a name substituted into a{0}placeholder. Two of the three could be reached with a value that had been HTML-decoded on the way in, so the escaping the API applied no longer held at render time.Rather than filter what reaches those, they now use text interpolation, which removes the sink. I checked every assignment to these dialog objects and every locale file that supplies the strings, in all languages present in the repo: none contain tags, so nothing renders differently.
Scope
The sweep covered
v-htmlbindings, jQuery.html(<var>),innerHTML,insertAdjacentHTML, and everyunescapeHtmlcall site, in this repo and in the enterprise plugins, cross-referencing each sink against whether its source is API-escaped or not.countly.models.jsactions columnvue/components/vis.jschart tooltips<textarea v-html>in hooks effectsformatTimeAgo/ i18n-only bindingsv-htmlbindingscountly.template.jsandappIdsToNamesconsumersCounts, so the sweep's completeness is checkable: 62
v-htmlbindings here and 60 in the enterprise plugins, 57.html()hits of which 12 are application code, 45innerHTML-family hits of which about 20 are application code, and 137unescapeHtmlcall sites classified by destination.Worth recording for future readers: a detached-element
.html()is still a live sink, since image loading is not gated on document insertion. The ones above are inert because of their inputs, not because they are detached.Verification
New file
test/unit-tests/plugins.compliance-hub.actions-escaping.jsloads the real module in a sandbox and exercises the actualonReadybuilder, so it tests the shipped code path rather than a copy. It pins both directions, because the failure mode here is symmetric: a value carrying markup must be neutralized, and an already-escaped API value must not be double-escaped.Countly Request, network dependent). Baseline established by re-running with the new file excluded, not assumed.Propagation
The compliance-hub change applies to
release.24.05and to countly-platformmain, where the same line exists unchanged. The dialog templates want checking there too. Backports to follow separately rather than widening this PR.🤖 Generated with Claude Code