From bd40b0ff695a9b71d12982d2bc2ed8b984437716 Mon Sep 17 00:00:00 2001
From: JackUait
Date: Mon, 10 Nov 2025 20:50:34 +0300
Subject: [PATCH] test: add tests for sanitisation
---
.../block-tunes/block-tune-move-up.ts | 4 +-
src/components/core.ts | 6 +-
src/components/dom.ts | 8 +-
src/components/modules/blockEvents.ts | 8 +-
src/components/modules/blockManager.ts | 18 +-
src/components/modules/blockSelection.ts | 6 +-
src/components/modules/paste.ts | 18 +-
src/components/modules/rectangleSelection.ts | 2 +-
src/components/modules/saver.ts | 14 +-
src/components/polyfills.ts | 16 +-
src/components/selection.ts | 5 +-
src/components/utils/sanitizer.ts | 321 +++++-
test/playwright/tests/sanitisation.spec.ts | 995 +++++++++++++++++-
13 files changed, 1308 insertions(+), 113 deletions(-)
diff --git a/src/components/block-tunes/block-tune-move-up.ts b/src/components/block-tunes/block-tune-move-up.ts
index e87d3126..af0fd136 100644
--- a/src/components/block-tunes/block-tune-move-up.ts
+++ b/src/components/block-tunes/block-tune-move-up.ts
@@ -74,8 +74,8 @@ export default class MoveUpTune implements BlockTune {
* - when previous block is visible and has offset from the window,
* than we scroll window to the difference between this offsets.
*/
- const currentBlockCoords = currentBlockElement.getBoundingClientRect(),
- previousBlockCoords = previousBlockElement.getBoundingClientRect();
+ const currentBlockCoords = currentBlockElement.getBoundingClientRect();
+ const previousBlockCoords = previousBlockElement.getBoundingClientRect();
let scrollUpOffset;
diff --git a/src/components/core.ts b/src/components/core.ts
index dbf9c3af..128ee5a1 100644
--- a/src/components/core.ts
+++ b/src/components/core.ts
@@ -155,11 +155,7 @@ export default class Core {
};
this.config.placeholder = this.config.placeholder ?? false;
- this.config.sanitizer = this.config.sanitizer || {
- p: true,
- b: true,
- a: true,
- } as SanitizerConfig;
+ this.config.sanitizer = this.config.sanitizer ?? {} as SanitizerConfig;
this.config.hideToolbar = this.config.hideToolbar ?? false;
this.config.tools = this.config.tools || {};
diff --git a/src/components/dom.ts b/src/components/dom.ts
index 67c06d1c..ac07fb77 100644
--- a/src/components/dom.ts
+++ b/src/components/dom.ts
@@ -132,8 +132,8 @@ export default class Dom {
*/
public static swap(el1: HTMLElement, el2: HTMLElement): void {
// create marker element and insert it where el1 is
- const temp = document.createElement('div'),
- parent = el1.parentNode;
+ const temp = document.createElement('div');
+ const parent = el1.parentNode;
parent?.insertBefore(temp, el1);
@@ -231,8 +231,8 @@ export default class Dom {
*
* @type {string}
*/
- const child = atLast ? 'lastChild' : 'firstChild',
- sibling = atLast ? 'previousSibling' : 'nextSibling';
+ const child = atLast ? 'lastChild' : 'firstChild';
+ const sibling = atLast ? 'previousSibling' : 'nextSibling';
if (node === null || node.nodeType !== Node.ELEMENT_NODE) {
return node;
diff --git a/src/components/modules/blockEvents.ts b/src/components/modules/blockEvents.ts
index 40e0973e..cd9546af 100644
--- a/src/components/modules/blockEvents.ts
+++ b/src/components/modules/blockEvents.ts
@@ -657,10 +657,10 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keyboard event
*/
private needToolbarClosing(event: KeyboardEvent): boolean {
- const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened),
- blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened),
- inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened),
- flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
+ const toolboxItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.Toolbar.toolbox.opened);
+ const blockSettingsItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.BlockSettings.opened);
+ const inlineToolbarItemSelected = (event.keyCode === _.keyCodes.ENTER && this.Editor.InlineToolbar.opened);
+ const flippingToolbarItems = event.keyCode === _.keyCodes.TAB;
/**
* Do not close Toolbar in cases:
diff --git a/src/components/modules/blockManager.ts b/src/components/modules/blockManager.ts
index feb34e5a..d1da4c10 100644
--- a/src/components/modules/blockManager.ts
+++ b/src/components/modules/blockManager.ts
@@ -9,7 +9,7 @@ import Module from '../__module';
import $ from '../dom';
import * as _ from '../utils';
import Blocks from '../blocks';
-import type { BlockToolData, PasteEvent } from '../../../types';
+import type { BlockToolData, PasteEvent, SanitizerConfig } from '../../../types';
import type { BlockTuneData } from '../../../types/block-tunes/block-tune-data';
import BlockAPI from '../block/api';
import type { BlockMutationEventMap, BlockMutationType } from '../../../types/events/block';
@@ -18,7 +18,7 @@ import { BlockAddedMutationType } from '../../../types/events/block/BlockAdded';
import { BlockMovedMutationType } from '../../../types/events/block/BlockMoved';
import { BlockChangedMutationType } from '../../../types/events/block/BlockChanged';
import { BlockChanged } from '../events';
-import { clean, sanitizeBlocks } from '../utils/sanitizer';
+import { clean, composeSanitizerConfig, sanitizeBlocks } from '../utils/sanitizer';
import { convertStringToBlockData, isBlockConvertable } from '../utils/blocks';
import PromiseQueue from '../utils/promise-queue';
@@ -493,7 +493,11 @@ export default class BlockManager extends Module {
return;
}
- const [ cleanData ] = sanitizeBlocks([ blockToMergeDataRaw ], targetBlock.tool.sanitizeConfig);
+ const [ cleanData ] = sanitizeBlocks(
+ [ blockToMergeDataRaw ],
+ targetBlock.tool.sanitizeConfig,
+ this.config.sanitizer as SanitizerConfig
+ );
blockToMergeData = cleanData;
@@ -689,9 +693,9 @@ export default class BlockManager extends Module {
element = element.parentNode as HTMLElement;
}
- const nodes = this._blocks.nodes,
- firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),
- index = nodes.indexOf(firstLevelBlock as HTMLElement);
+ const nodes = this._blocks.nodes;
+ const firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`);
+ const index = nodes.indexOf(firstLevelBlock as HTMLElement);
if (index >= 0) {
return this._blocks[index];
@@ -857,7 +861,7 @@ export default class BlockManager extends Module {
*/
const cleanData: string = clean(
exportedData,
- replacingTool.sanitizeConfig
+ composeSanitizerConfig(this.config.sanitizer as SanitizerConfig, replacingTool.sanitizeConfig)
);
/**
diff --git a/src/components/modules/blockSelection.ts b/src/components/modules/blockSelection.ts
index a6edcb8c..66fffcb3 100644
--- a/src/components/modules/blockSelection.ts
+++ b/src/components/modules/blockSelection.ts
@@ -12,7 +12,7 @@ import Shortcuts from '../utils/shortcuts';
import SelectionUtils from '../selection';
import type { SanitizerConfig } from '../../../types/configs';
-import { clean } from '../utils/sanitizer';
+import { clean, composeSanitizerConfig } from '../utils/sanitizer';
/**
*
@@ -33,7 +33,7 @@ export default class BlockSelection extends Module {
* @returns {SanitizerConfig}
*/
private get sanitizerConfig(): SanitizerConfig {
- return {
+ const baseConfig: SanitizerConfig = {
p: {},
h1: {},
h2: {},
@@ -57,6 +57,8 @@ export default class BlockSelection extends Module {
i: {},
u: {},
};
+
+ return composeSanitizerConfig(this.config.sanitizer as SanitizerConfig, baseConfig);
}
/**
diff --git a/src/components/modules/paste.ts b/src/components/modules/paste.ts
index 6c7ec442..a75f3e87 100644
--- a/src/components/modules/paste.ts
+++ b/src/components/modules/paste.ts
@@ -10,7 +10,7 @@ import type {
} from '../../../types';
import type Block from '../block';
import type { SavedData } from '../../../types/data-formats';
-import { clean, sanitizeBlocks } from '../utils/sanitizer';
+import { clean, composeSanitizerConfig, sanitizeBlocks } from '../utils/sanitizer';
import type BlockToolAdapter from '../tools/block';
/**
@@ -214,7 +214,13 @@ export default class Paste extends Module {
return result;
}, {} as SanitizerConfig);
- const customConfig = Object.assign({}, toolsTags, Tools.getAllInlineToolsSanitizeConfig(), { br: {} });
+ const inlineSanitizeConfig = Tools.getAllInlineToolsSanitizeConfig();
+ const customConfig = composeSanitizerConfig(
+ this.config.sanitizer as SanitizerConfig,
+ toolsTags,
+ inlineSanitizeConfig,
+ { br: {} }
+ );
const cleanData = clean(htmlData, customConfig);
/** If there is no HTML or HTML string is equal to plain one, process it as plain text */
@@ -608,7 +614,7 @@ export default class Paste extends Module {
return nodes
.map((node) => {
- let content: HTMLElement | null | undefined, tool = Tools.defaultTool, isBlock = false;
+ let content: HTMLElement | null | undefined; let tool = Tools.defaultTool; let isBlock = false;
switch (node.nodeType) {
/** If node is a document fragment, use temp wrapper to get innerHTML */
@@ -897,8 +903,10 @@ export default class Paste extends Module {
*/
private insertEditorJSData(blocks: Pick[]): void {
const { BlockManager, Caret, Tools } = this.Editor;
- const sanitizedBlocks = sanitizeBlocks(blocks, (name) =>
- Tools.blockTools.get(name)?.sanitizeConfig ?? {}
+ const sanitizedBlocks = sanitizeBlocks(
+ blocks,
+ (name) => Tools.blockTools.get(name)?.sanitizeConfig ?? {},
+ this.config.sanitizer as SanitizerConfig
);
sanitizedBlocks.forEach(({ tool, data }, i) => {
diff --git a/src/components/modules/rectangleSelection.ts b/src/components/modules/rectangleSelection.ts
index d545036d..20cda624 100644
--- a/src/components/modules/rectangleSelection.ts
+++ b/src/components/modules/rectangleSelection.ts
@@ -480,7 +480,7 @@ export default class RectangleSelection extends Module {
private trySelectNextBlock(index): void {
const sameBlock = this.stackOfSelected[this.stackOfSelected.length - 1] === index;
const sizeStack = this.stackOfSelected.length;
- const down = 1, up = -1, undef = 0;
+ const down = 1; const up = -1; const undef = 0;
if (sameBlock) {
return;
diff --git a/src/components/modules/saver.ts b/src/components/modules/saver.ts
index c0219283..5aff5654 100644
--- a/src/components/modules/saver.ts
+++ b/src/components/modules/saver.ts
@@ -6,7 +6,7 @@
* @version 2.0.0
*/
import Module from '../__module';
-import type { OutputData } from '../../../types';
+import type { OutputData, SanitizerConfig } from '../../../types';
import type { SavedData, ValidatedData } from '../../../types/data-formats';
import type Block from '../block';
import * as _ from '../utils';
@@ -28,8 +28,8 @@ export default class Saver extends Module {
*/
public async save(): Promise {
const { BlockManager, Tools } = this.Editor;
- const blocks = BlockManager.blocks,
- chainData = [];
+ const blocks = BlockManager.blocks;
+ const chainData = [];
try {
blocks.forEach((block: Block) => {
@@ -37,9 +37,11 @@ export default class Saver extends Module {
});
const extractedData = await Promise.all(chainData) as Array>;
- const sanitizedData = await sanitizeBlocks(extractedData, (name) => {
- return Tools.blockTools.get(name).sanitizeConfig;
- });
+ const sanitizedData = await sanitizeBlocks(
+ extractedData,
+ (name) => Tools.blockTools.get(name).sanitizeConfig,
+ this.config.sanitizer as SanitizerConfig
+ );
return this.makeOutput(sanitizedData);
} catch (e) {
diff --git a/src/components/polyfills.ts b/src/components/polyfills.ts
index c8aeac2c..46afb0f3 100644
--- a/src/components/polyfills.ts
+++ b/src/components/polyfills.ts
@@ -130,14 +130,14 @@ if (typeof Element.prototype.scrollIntoViewIfNeeded === 'undefined') {
return;
}
- const parentComputedStyle = window.getComputedStyle(parent, null),
- parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
- parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
- overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
- overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
- overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
- overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
- alignWithTop = overTop && !overBottom;
+ const parentComputedStyle = window.getComputedStyle(parent, null);
+ const parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width'));
+ const parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width'));
+ const overTop = this.offsetTop - parent.offsetTop < parent.scrollTop;
+ const overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight);
+ const overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft;
+ const overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth);
+ const alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
diff --git a/src/components/selection.ts b/src/components/selection.ts
index a86f6ef7..8a507301 100644
--- a/src/components/selection.ts
+++ b/src/components/selection.ts
@@ -234,8 +234,8 @@ export default class SelectionUtils {
* @returns {DOMRect}
*/
public static get rect(): DOMRect {
- let sel: Selection | MSSelection | undefined | null = (document as Document).selection,
- range: TextRange | Range;
+ let sel: Selection | MSSelection | undefined | null = (document as Document).selection;
+ let range: TextRange | Range;
let rect = {
x: 0,
@@ -414,6 +414,7 @@ export default class SelectionUtils {
public removeFakeBackground(): void {
if (!this.fakeBackgroundElements.length) {
this.isFakeBackgroundEnabled = false;
+
return;
}
diff --git a/src/components/utils/sanitizer.ts b/src/components/utils/sanitizer.ts
index 69d50149..aaa5af43 100644
--- a/src/components/utils/sanitizer.ts
+++ b/src/components/utils/sanitizer.ts
@@ -30,9 +30,13 @@ import * as _ from '../utils';
*/
import HTMLJanitor from 'html-janitor';
-import type { BlockToolData, SanitizerConfig } from '../../../types';
+import type { BlockToolData, SanitizerConfig, SanitizerRule } from '../../../types';
import type { SavedData } from '../../../types/data-formats';
+type DeepSanitizerRule = SanitizerConfig | SanitizerRule | boolean;
+
+const UNSAFE_URL_ATTR_PATTERN = /\s*(href|src)\s*=\s*(["']?)\s*(?:javascript:|data:text\/html)[^"' >]*\2/gi;
+
/**
* Sanitize Blocks
*
@@ -40,23 +44,27 @@ import type { SavedData } from '../../../types/data-formats';
*
* @param blocksData - blocks' data to sanitize
* @param sanitizeConfig — sanitize config to use or function to get config for Tool
+ * @param globalSanitizer — global sanitizer config defined on editor level
*/
-export function sanitizeBlocks(
+export const sanitizeBlocks = (
blocksData: Array>,
- sanitizeConfig: SanitizerConfig | ((toolName: string) => SanitizerConfig)
-): Array> {
+ sanitizeConfig: SanitizerConfig | ((toolName: string) => SanitizerConfig | undefined),
+ globalSanitizer: SanitizerConfig = {} as SanitizerConfig
+): Array> => {
return blocksData.map((block) => {
const toolConfig = _.isFunction(sanitizeConfig) ? sanitizeConfig(block.tool) : sanitizeConfig;
+ const rules = toolConfig ?? ({} as SanitizerConfig);
- if (_.isEmpty(toolConfig)) {
+ if (_.isEmpty(rules) && _.isEmpty(globalSanitizer)) {
return block;
}
- block.data = deepSanitize(block.data, toolConfig) as BlockToolData;
-
- return block;
+ return {
+ ...block,
+ data: deepSanitize(block.data, rules, globalSanitizer) as BlockToolData,
+ };
});
-}
+};
/**
* Cleans string from unwanted tags
* Method allows to use default config
@@ -65,7 +73,7 @@ export function sanitizeBlocks(
* @param {SanitizerConfig} customConfig - allowed tags
* @returns {string} clean HTML
*/
-export function clean(taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string {
+export const clean = (taintString: string, customConfig: SanitizerConfig = {} as SanitizerConfig): string => {
const sanitizerConfig = {
tags: customConfig,
};
@@ -76,15 +84,20 @@ export function clean(taintString: string, customConfig: SanitizerConfig = {} as
const sanitizerInstance = new HTMLJanitor(sanitizerConfig);
return sanitizerInstance.clean(taintString);
-}
+};
/**
* Method recursively reduces Block's data and cleans with passed rules
*
* @param {BlockToolData|object|*} dataToSanitize - taint string or object/array that contains taint string
* @param {SanitizerConfig} rules - object with sanitizer rules
+ * @param {SanitizerConfig} globalRules - global sanitizer config
*/
-function deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig): object | string {
+const deepSanitize = (
+ dataToSanitize: object | string,
+ rules: DeepSanitizerRule,
+ globalRules: SanitizerConfig
+): object | string => {
/**
* BlockData It may contain 3 types:
* - Array
@@ -95,12 +108,12 @@ function deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig):
/**
* Array: call sanitize for each item
*/
- return cleanArray(dataToSanitize, rules);
+ return cleanArray(dataToSanitize, rules, globalRules);
} else if (_.isObject(dataToSanitize)) {
/**
* Objects: just clean object deeper.
*/
- return cleanObject(dataToSanitize, rules);
+ return cleanObject(dataToSanitize, rules, globalRules);
} else {
/**
* Primitives (number|string|boolean): clean this item
@@ -108,32 +121,42 @@ function deepSanitize(dataToSanitize: object | string, rules: SanitizerConfig):
* Clean only strings
*/
if (_.isString(dataToSanitize)) {
- return cleanOneItem(dataToSanitize, rules);
+ return cleanOneItem(dataToSanitize, rules, globalRules);
}
return dataToSanitize;
}
-}
+};
/**
* Clean array
*
* @param {Array} array - [1, 2, {}, []]
* @param {SanitizerConfig} ruleForItem - sanitizer config for array
+ * @param globalRules
*/
-function cleanArray(array: Array
',
+ });
+
+ const output = await saveEditor(page);
+ const text = output.blocks[0].data.text;
+
+ expect(text).not.toContain('