jodit
Version:
Jodit is an awesome and useful wysiwyg editor with filebrowser
184 lines (183 loc) • 6.61 kB
JavaScript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* @module helpers/html
*/
import { Dom } from "../../dom/dom.js";
import { attr } from "../utils/index.js";
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
/**
* Integration points where HTML is legitimately allowed inside MathML/SVG foreign content.
*/
const HTML_INTEGRATION_POINTS = new Set([
'foreignobject',
'annotation-xml',
'desc',
'title'
]);
/**
* True for an HTML element the parser placed inside MathML/SVG outside an integration point - smuggled
* HTML (e.g. `mglyph` / `style` under `<math>`) that a reparse can hoist into a live node. Legitimate
* MathML/SVG children and HTML below an integration point are kept.
*/
function isSmuggledForeignHtml(elm) {
if (elm.namespaceURI !== HTML_NAMESPACE ||
elm.closest('math,svg') == null) {
return false;
}
for (let parent = elm.parentElement; parent; parent = parent.parentElement) {
const name = parent.nodeName.toLowerCase();
if (name === 'math' || name === 'svg') {
break;
}
if (HTML_INTEGRATION_POINTS.has(name)) {
return false;
}
}
return true;
}
/**
* Removes dangerous constructs from HTML
*/
export function safeHTML(box, options) {
var _a;
if (!Dom.isElement(box) && !Dom.isFragment(box)) {
return;
}
// Drop HTML smuggled into MathML/SVG before the walk: a reparse would otherwise hoist its hidden
// markup into a live node.
const foreign = box.querySelectorAll('math *, svg *');
for (let i = 0; i < foreign.length; i++) {
if (box.contains(foreign[i]) && isSmuggledForeignHtml(foreign[i])) {
Dom.safeRemove(foreign[i]);
}
}
const removeEvents = (_a = options.removeEventAttributes) !== null && _a !== void 0 ? _a : options.removeOnError;
// Single synchronous traversal of the subtree. Besides removing event
// handlers and `javascript:` links, `sanitizeHTMLElement` neutralises
// executable `iframe[srcdoc]`, `data:text/html` / SVG `data:` document
// sources and dangerous schemes in every URL-bearing attribute.
const process = (node) => {
if (!Dom.isElement(node)) {
return;
}
if (removeEvents) {
removeAllEventAttributes(node);
}
sanitizeHTMLElement(node, options);
if (options.safeLinksTarget &&
node.nodeName === 'A' &&
node.getAttribute('target') === '_blank') {
const rel = node.getAttribute('rel') || '';
const parts = rel.split(/\s+/).filter(Boolean);
if (!parts.includes('noopener')) {
parts.push('noopener');
}
if (!parts.includes('noreferrer')) {
parts.push('noreferrer');
}
attr(node, 'rel', parts.join(' '));
}
};
process(box);
Dom.each(box, process);
}
/**
* Remove all on* event handler attributes from an element
*/
function removeAllEventAttributes(elm) {
if (!Dom.isElement(elm)) {
return false;
}
let effected = false;
const toRemove = [];
for (let i = 0; i < elm.attributes.length; i++) {
if (elm.attributes[i].name.toLowerCase().startsWith('on')) {
toRemove.push(elm.attributes[i].name);
}
}
for (const name of toRemove) {
elm.removeAttribute(name);
effected = true;
}
return effected;
}
/**
* URL-bearing attributes (besides `href`) that can load or execute content.
*/
const URL_ATTRIBUTES = [
'src',
'data',
'action',
'formaction',
'poster',
'background',
'xlink:href'
];
/**
* Tags that load their URL as a *document* (scripts inside run). An SVG data
* URL is only an XSS vector here — as an `<img>` source it renders inertly.
*/
const DOCUMENT_EMBED_TAGS = new Set(['iframe', 'frame', 'object', 'embed']);
/**
* Detects executable / script-bearing URL schemes. The attribute value is
* already HTML-entity-decoded by `getAttribute`, so only whitespace and
* control characters (which browsers ignore inside a scheme) need stripping.
*/
function isDangerousUrl(value, tagName) {
// eslint-disable-next-line no-control-regex
const normalized = value.replace(/[\u0000-\u0020]+/g, '').toLowerCase();
if (/^(?:javascript|vbscript|livescript|mocha):/.test(normalized)) {
return true;
}
if (/^data:(?:text\/html|application\/xhtml)/.test(normalized)) {
return true;
}
return (/^data:image\/svg/.test(normalized) && DOCUMENT_EMBED_TAGS.has(tagName));
}
export function sanitizeHTMLElement(elm, { safeJavaScriptLink, removeOnError } = {
safeJavaScriptLink: true,
removeOnError: true
}) {
if (!Dom.isElement(elm)) {
return false;
}
let effected = false;
if (removeOnError && elm.hasAttribute('onerror')) {
attr(elm, 'onerror', null);
effected = true;
}
const tagName = elm.nodeName.toLowerCase();
const href = elm.getAttribute('href');
// Neutralize executable-scheme `href`s with the same normalization used for
// every other URL attribute (`isDangerousUrl`), which strips control bytes,
// tabs and newlines and lowercases before matching the scheme. The previous
// bare `href.trim().indexOf('javascript') === 0` was case-sensitive and
// missed `JAVASCRIPT:`, a leading control byte, or a tab/newline inside the
// scheme (e.g. `java\tscript:`) — all of which the browser still resolves to
// `javascript:` on click. See GHSA-j839-gqq4-gf9j.
if (safeJavaScriptLink && href && isDangerousUrl(href, tagName)) {
const protocol = typeof location !== 'undefined' ? location.protocol : 'http:';
attr(elm, 'href', protocol + '//' + href);
effected = true;
}
if (safeJavaScriptLink) {
// `srcdoc` runs its content as a full HTML document — drop it entirely.
if (elm.hasAttribute('srcdoc')) {
attr(elm, 'srcdoc', null);
effected = true;
}
// Strip executable schemes from any other URL-bearing attribute.
for (const name of URL_ATTRIBUTES) {
const value = elm.getAttribute(name);
if (value && isDangerousUrl(value, tagName)) {
attr(elm, name, null);
effected = true;
}
}
}
return effected;
}