Files
dc/client/src/app/shared/utils/col-info-html.ts
T
dc c9eed6dae7
Build / Build-and-ng-test (pull_request) Successful in 5m20s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m10s
Build / Build-and-test-development (pull_request) Successful in 25m30s
fix(security): escape col-info dropdown and origin-check VA replay
buildColInfoHtml interpolated server/DB-controlled column labels, formats and
DQ RULE_VALUE (regex/formula) strings into HTML assigned to raw DOM
elem.innerHTML in both viewer.component.ts and editor.component.ts. A user who
can author a validation rule (or a column label) could store markup that runs
in the browser of any editor/approver who opens a column info dropdown - the
same class of stored XSS fixed for the status renderers in #319. Escape every
interpolated field via the same escapeHtml approach.

The pre-bootstrap VA listener (va-early.js) stores any-origin postMessage in
window.__vaLastMessage; replayEarlyMessages replayed it into the editor filter
without the live path's isTrustedSource check. Add isTrustedEarlyOrigin so the
replay only accepts a message from this origin or the embedding frame's origin
(document.referrer), mirroring the live handler.

Regression tests: col-info-html.spec.ts proves the injected element does not
survive (fails on the old impl, 5of7 rando-fail -> all pass), preserving the
10 existing behaviour tests; full Angular suite 523/523 green; production
build (AOT) compiles clean.
2026-09-19 16:28:21 +00:00

70 lines
2.7 KiB
TypeScript

import { DataFormat } from '../../models/sas/common/DateFormat'
/**
* Returns string-safe text of any value so it can be concatenated into a
* string that is later assigned to raw DOM innerHTML. The column metadata
* (label/format) and DQ RULE_VALUE strings (regex/formula) are DB-controlled -
* a validation-rule author can store markup such as
* `<img src=x onerror=...>` in a HARDREGEX value or a column label - so they
* must never be parsed as HTML by the browser. Escaping turns any embedded
* markup into inert text.
*/
const escapeHtml = (value: any): string =>
String(value ?? '').replace(/[&<>"']/g, (char) => {
const entities: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}
return entities[char]
})
/**
* Builds the HTML shown in a column-header "info" dropdown item (viewer and
* editor). NAME is listed first so it's visible regardless of whether
* headers are currently displayed as NAME or LABEL.
*
* The returned string is assigned to raw DOM `elem.innerHTML` by both callers
* (viewer.component.ts / editor.component.ts) - every field interpolated below
* is therefore escaped via escapeHtml, since no Angular sanitizer runs on a
* raw innerHTML assignment.
*/
export function buildColInfoHtml(
colName: string,
colInfo?: DataFormat,
hardRegexValue?: string,
softRegexValue?: string,
formulaValue?: string
): string {
if (!colInfo) return 'No info found'
let html = `NAME: ${escapeHtml(colName)}<br>LABEL: ${escapeHtml(colInfo.label)}<br>TYPE: ${escapeHtml(colInfo.type)}<br>LENGTH: ${escapeHtml(colInfo.length)}<br>FORMAT: ${escapeHtml(colInfo.format)}`
// Only ever one REGEX rule is applied per column: when both HARDREGEX
// and SOFTREGEX exist, SOFTREGEX is ignored entirely (same precedence as
// makeRegexWarningRenderer / DcValidator.failsSoftRegex). Show only the
// rule that is applied.
if (hardRegexValue) {
html += `<br>HARDREGEX: ${escapeHtml(hardRegexValue)}`
} else if (softRegexValue) {
html += `<br>SOFTREGEX: ${escapeHtml(softRegexValue)}`
}
// '√x=' stands in for a text label here - HARDFORMULA vs SOFTFORMULA is
// already conveyed by the column's readOnly state, so there's no need to
// spell out which one this is. formulaValue is the raw RULE_VALUE, which
// may or may not include its own leading '=' (see parseFormulaRule.ts -
// it's the rule author's choice, not inserted) - strip it here so it's
// never doubled against this label's own '='.
if (formulaValue) {
const formula = formulaValue.startsWith('=')
? formulaValue.slice(1)
: formulaValue
html += `<br>√x=${escapeHtml(formula)}`
}
return html
}