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.
70 lines
2.7 KiB
TypeScript
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> = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}
|
|
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
|
|
}
|