Skip to content

Commit

Permalink
fix: harden \htmlData against XSS
Browse files Browse the repository at this point in the history
  • Loading branch information
arnog committed Jan 18, 2025
1 parent 4f34e3c commit abc2605
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 42 deletions.
66 changes: 56 additions & 10 deletions src/core/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,15 +423,17 @@ export class Box implements BoxInterface {
.filter((x, e, a) => x.length > 0 && a.indexOf(x) === e)
.join(' ');

if (classList.length > 0) props += ` class="${classList}"`;
if (classList.length > 0)
props += ` class=${sanitizeAttributeValue(`"${classList}"`)}`;

//
// 3.2 Id
//
if (this.id) props += ` data-atom-id=${this.id}`;
if (this.id) props += ` data-atom-id=${sanitizeAttributeValue(this.id)}`;

// A (HTML5) CSS id may not contain a space
if (this.cssId) props += ` id="${this.cssId.replace(/ /g, '-')}" `;
if (this.cssId)
props += ` id=${sanitizeAttributeValue(`"${this.cssId.replace(/ /g, '-')}"`)}`;

//
// 3.3 Attributes
Expand All @@ -440,7 +442,10 @@ export class Box implements BoxInterface {
props +=
' ' +
Object.keys(this.attributes)
.map((x) => `${x}="${this.attributes![x]}"`)
.map(
(x) =>
`${sanitizeAttributeName(x)}=${sanitizeAttributeValue(`"${this.attributes![x]}"`)}`
)
.join(' ');
}

Expand All @@ -449,11 +454,12 @@ export class Box implements BoxInterface {
for (const entry of entries) {
const matched = entry.match(/([^=]+)=(.+$)/);
if (matched) {
const key = matched[1].trim().replace(/ /g, '-');
if (key) props += ` data-${key}="${matched[2]}" `;
const key = sanitizeAttributeName(matched[1]);
const value = sanitizeAttributeValue(matched[2]);
if (key) props += ` ${key}=${value}`;
} else {
const key = entry.trim().replace(/ /g, '-');
if (key) props += ` data-${key} `;
const key = sanitizeAttributeName(entry);
if (key) props += ` ${key} `;
}
}
}
Expand Down Expand Up @@ -488,10 +494,12 @@ export class Box implements BoxInterface {
if (key) styleString += `${key}:${matched[2]};`;
}
}
if (styleString) props += ` style="${styleString}"`;
if (styleString)
props += ` style=${sanitizeAttributeValue(`"${styleString}"`)};`;
}

if (styles.length > 0) props += ` style="${styles.join(';')}"`;
if (styles.length > 0)
props += ` style=${sanitizeAttributeValue(`"${styles.join(';')}"`)}`;

//
// 4. Tag markup
Expand Down Expand Up @@ -736,3 +744,41 @@ function horizontalLayout(box: Box, fontName: FontName): void {
box.maxFontSize = maxFontSize;
}
}

function sanitizeAttributeName(attribute: string): string {
attribute = attribute.trim().replace(/ /g, '-');

/**
* https://w3c.github.io/html-reference/syntax.html#syntax-attributes
*
* > Attribute Names must consist of one or more characters
* other than the space characters, U+0000 NULL,
* '"', "'", ">", "/", "=", the control characters,
* and any characters that are not defined by Unicode.
*/
const invalidAttributeNameRegex = /[\s"'>/=\x00-\x1f]/;
if (invalidAttributeNameRegex.test(attribute))
throw new Error(`Invalid attribute name: ${attribute}`);

return attribute;
}

function sanitizeAttributeValue(value: string): string {
value = value.trim();

if (value.startsWith('"') && value.endsWith('"')) {
// Must not contain any `"`
if (value.slice(1, -1).match(/"/))
throw new Error(`Invalid attribute value: ${value}`);
} else if (value.startsWith("'") && value.endsWith("'")) {
// Must not contain any `'`
if (value.slice(1, -1).match(/'/))
throw new Error(`Invalid attribute value: ${value}`);
} else {
// Must not contain any literal space characters, `"`, `'`, `=`, `>`, `<` or backtick characters
if (value.match(/[\s"'`=><`]/))
throw new Error(`Invalid attribute value: ${value}`);
}

return value;
}
67 changes: 36 additions & 31 deletions src/editor-mathfield/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,41 +102,46 @@ export function contentMarkup(
mathfield: _Mathfield,
renderOptions?: { forHighlighting?: boolean; interactive?: boolean }
): string {
//
// 1. Update selection state and blinking cursor (caret)
//
const { model } = mathfield;
model.root.caret = undefined;
model.root.isSelected = false;
model.root.containsCaret = true;
for (const atom of model.atoms) {
atom.caret = undefined;
atom.isSelected = false;
atom.containsCaret = false;
}
if (model.selectionIsCollapsed) {
const atom = model.at(model.position);
atom.caret = mathfield.model.mode;
let ancestor = atom.parent;
while (ancestor) {
ancestor.containsCaret = true;
ancestor = ancestor.parent;
try {
//
// 1. Update selection state and blinking cursor (caret)
//
const { model } = mathfield;
model.root.caret = undefined;
model.root.isSelected = false;
model.root.containsCaret = true;
for (const atom of model.atoms) {
atom.caret = undefined;
atom.isSelected = false;
atom.containsCaret = false;
}
if (model.selectionIsCollapsed) {
const atom = model.at(model.position);
atom.caret = mathfield.model.mode;
let ancestor = atom.parent;
while (ancestor) {
ancestor.containsCaret = true;
ancestor = ancestor.parent;
}
} else {
const atoms = model.getAtoms(model.selection, { includeChildren: true });
for (const atom of atoms) atom.isSelected = true;
}
} else {
const atoms = model.getAtoms(model.selection, { includeChildren: true });
for (const atom of atoms) atom.isSelected = true;
}

//
// 2. Render a box representation of the mathfield content
//
const box = makeBox(mathfield, renderOptions);
//
// 2. Render a box representation of the mathfield content
//
const box = makeBox(mathfield, renderOptions);

//
// 3. Generate markup
//
//
// 3. Generate markup
//

return box.toMarkup();
return box.toMarkup();
} catch (e) {
console.error(e);
return '<span class="ML__latex" translate="no" aria-hidden="true">💣</span>';
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/public/mathfield-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ export class MathfieldElement extends HTMLElement implements Mathfield {
}

/**
* Support for [Trusted Type](https://w3c.github.io/webappsec-trusted-types/dist/spec/).
* Support for [Trusted Type](https://www.w3.org/TR/trusted-types/).
*
* This optional function will be called before a string of HTML is
* injected in the DOM, allowing that string to be sanitized
Expand Down

0 comments on commit abc2605

Please sign in to comment.