diff --git a/src/core/box.ts b/src/core/box.ts index 219792fc0..8e570cecd 100644 --- a/src/core/box.ts +++ b/src/core/box.ts @@ -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 @@ -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(' '); } @@ -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} `; } } } @@ -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 @@ -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; +} diff --git a/src/editor-mathfield/render.ts b/src/editor-mathfield/render.ts index f6c4a2b84..9e2052e8f 100644 --- a/src/editor-mathfield/render.ts +++ b/src/editor-mathfield/render.ts @@ -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 ''; + } } /** diff --git a/src/public/mathfield-element.ts b/src/public/mathfield-element.ts index 7885d37b2..2cc8bce2f 100644 --- a/src/public/mathfield-element.ts +++ b/src/public/mathfield-element.ts @@ -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