VYPR
Moderate severityNVD Advisory· Published Dec 15, 2022· Updated Apr 17, 2025

editor.js contains Code Injection

CVE-2022-23474

Description

Editor.js is a block-style editor with clean JSON output. Versions prior to 2.26.0 are vulnerable to Code Injection via pasted input. The processHTML method passes pasted input into wrapper’s innerHTML. This issue is patched in version 2.26.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Editor.js prior to 2.26.0 is vulnerable to code injection via pasted input due to unsafe use of innerHTML in the processHTML method.

Vulnerability

Overview

Editor.js versions prior to 2.26.0 contain a code injection vulnerability in the processHTML method. This method directly assigns user-supplied pasted content to the innerHTML property of a wrapper element without sanitization, allowing an attacker to inject arbitrary HTML and JavaScript. The root cause is the lack of input validation or escaping before DOM insertion [1][2].

Exploitation

An attacker can exploit this by crafting malicious HTML or JavaScript and pasting it into the editor. No authentication is required if the editor is publicly accessible. The pasted content is processed by processHTML, which inserts it into the DOM, causing the injected script to execute in the context of the user's session. The attack vector is straightforward and does not require any special network position beyond the ability to interact with the editor [1][2].

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the victim's browser. This can lead to data theft, session hijacking, defacement, or further attacks against the application and its users. The vulnerability is considered critical due to the ease of exploitation and potential for widespread impact [1][2].

Mitigation

The vulnerability is patched in Editor.js version 2.26.0. Users should upgrade immediately to this or a later version. The fix includes sanitization of pasted input, as evidenced by a commit that adds a sanitize configuration for paste tags [4]. No workarounds are documented; upgrading is the recommended action.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@editorjs/editorjsnpm
< 2.26.02.26.0

Affected products

3

Patches

1
f659015be6de

fix(tools-api): pasteConfig.tags now supports a sanitize config (#2100)

https://github.com/codex-team/editor.jsUmang G. PatelNov 21, 2022via ghsa
8 files changed · +629 46
  • docs/CHANGELOG.md+3 0 modified
    @@ -9,6 +9,9 @@
     - `Deprecated` — *Styles API* — CSS classes `.cdx-settings-button` and `.cdx-settings-button--active` are not recommended to use. Consider configuring your block settings with new JSON API instead.
     - `Fix` — Wrong element not highlighted anymore when popover opened.
     - `Fix` — When Tunes Menu open keydown events can not be handled inside plugins.
    +- `Fix` — If a Tool specifies some tags to substitute on paste, all attributes of that tags will be removed before passing them to the tool. Possible XSS vulnerability fixed.
    +- `Fix` — Workaround for the HTMLJanitor bug with Tables (https://github.com/guardian/html-janitor/issues/3) added
    +- `Improvement` — *Tools API* — `pasteConfig().tags` now support sanitizing configuration. It allows you to leave some explicitly specified attributes for pasted content.
     
     ### 2.25.0
     
    
  • docs/tools.md+22 2 modified
    @@ -151,7 +151,7 @@ To handle pasted HTML elements object returned from `pasteConfig` getter should
     
     For correct work you MUST provide `onPaste` handler at least for `defaultBlock` Tool.
     
    -> Example
    +#### Example
     
     Header Tool can handle `H1`-`H6` tags using paste handling API
     
    @@ -163,7 +163,27 @@ static get pasteConfig() {
     }
     ```
     
    -> Same tag can be handled by one (first specified) Tool only.
    +**Note. Same tag can be handled by one (first specified) Tool only.**
    +
    +**Note. All attributes of pasted tag will be removed. To leave some attribute, you should explicitly specify them. Se below**
    +
    +Let's suppose you want to leave the 'src' attribute when handle pasting of the `img` tags. Your config should look like this:
    +
    +```javascript
    +static get pasteConfig() {
    +  return {
    +    tags: [
    +      {
    +        img: {
    +          src: true
    +        }
    +      }
    +    ],
    +  }
    +}
    +```
    +
    +[Read more](https://editorjs.io/sanitizer) about the sanitizing configuration.
     
     ### RegExp patterns handling
     
    
  • src/components/dom.ts+9 7 modified
    @@ -53,7 +53,7 @@ export default class Dom {
        *
        * @returns {HTMLElement}
        */
    -  public static make(tagName: string, classNames: string|string[] = null, attributes: object = {}): HTMLElement {
    +  public static make(tagName: string, classNames: string | string[] = null, attributes: object = {}): HTMLElement {
         const el = document.createElement(tagName);
     
         if (Array.isArray(classNames)) {
    @@ -109,8 +109,8 @@ export default class Dom {
        * @param  {Element|Element[]|DocumentFragment|Text|Text[]} elements - element or elements list
        */
       public static append(
    -    parent: Element|DocumentFragment,
    -    elements: Element|Element[]|DocumentFragment|Text|Text[]
    +    parent: Element | DocumentFragment,
    +    elements: Element | Element[] | DocumentFragment | Text | Text[]
       ): void {
         if (Array.isArray(elements)) {
           elements.forEach((el) => parent.appendChild(el));
    @@ -125,7 +125,7 @@ export default class Dom {
        * @param {Element} parent - where to append
        * @param {Element|Element[]} elements - element or elements list
        */
    -  public static prepend(parent: Element, elements: Element|Element[]): void {
    +  public static prepend(parent: Element, elements: Element | Element[]): void {
         if (Array.isArray(elements)) {
           elements = elements.reverse();
           elements.forEach((el) => parent.prepend(el));
    @@ -168,7 +168,7 @@ export default class Dom {
        *
        * @returns {Element}
        */
    -  public static find(el: Element|Document = document, selector: string): Element {
    +  public static find(el: Element | Document = document, selector: string): Element {
         return el.querySelector(selector);
       }
     
    @@ -192,7 +192,7 @@ export default class Dom {
        *
        * @returns {NodeList}
        */
    -  public static findAll(el: Element|Document = document, selector: string): NodeList {
    +  public static findAll(el: Element | Document = document, selector: string): NodeList {
         return el.querySelectorAll(selector);
       }
     
    @@ -523,6 +523,8 @@ export default class Dom {
           'ruby',
           'section',
           'table',
    +      'tbody',
    +      'thead',
           'tr',
           'tfoot',
           'ul',
    @@ -619,7 +621,7 @@ export default class Dom {
        * @todo handle case when editor initialized in scrollable popup
        * @param el - element to compute offset
        */
    -  public static offset(el): {top: number; left: number; right: number; bottom: number} {
    +  public static offset(el): { top: number; left: number; right: number; bottom: number } {
         const rect = el.getBoundingClientRect();
         const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
         const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    
  • src/components/modules/paste.ts+148 30 modified
    @@ -4,7 +4,9 @@ import * as _ from '../utils';
     import {
       BlockAPI,
       PasteEvent,
    -  PasteEventDetail
    +  PasteEventDetail,
    +  SanitizerConfig,
    +  SanitizerRule
     } from '../../../types';
     import Block from '../block';
     import { SavedData } from '../../../types/data-formats';
    @@ -20,6 +22,12 @@ interface TagSubstitute {
        *
        */
       tool: BlockTool;
    +
    +  /**
    +   * If a Tool specifies just a tag name, all the attributes will be sanitized.
    +   * But Tool can explicitly specify sanitizer configuration for supported tags
    +   */
    +  sanitizationConfig?: SanitizerRule;
     }
     
     /**
    @@ -112,12 +120,12 @@ export default class Paste extends Module {
       /**
        * Tags` substitutions parameters
        */
    -  private toolsTags: {[tag: string]: TagSubstitute} = {};
    +  private toolsTags: { [tag: string]: TagSubstitute } = {};
     
       /**
        * Store tags to substitute by tool name
        */
    -  private tagsByTool: {[tools: string]: string[]} = {};
    +  private tagsByTool: { [tools: string]: string[] } = {};
     
       /** Patterns` substitutions parameters */
       private toolsPatterns: PatternSubstitute[] = [];
    @@ -186,7 +194,7 @@ export default class Paste extends Module {
             this.insertEditorJSData(JSON.parse(editorJSData));
     
             return;
    -      } catch (e) {} // Do nothing and continue execution as usual if error appears
    +      } catch (e) { } // Do nothing and continue execution as usual if error appears
         }
     
         /**
    @@ -198,7 +206,11 @@ export default class Paste extends Module {
     
         /** Add all tags that can be substituted to sanitizer configuration */
         const toolsTags = Object.keys(this.toolsTags).reduce((result, tag) => {
    -      result[tag.toLowerCase()] = true;
    +      /**
    +       * If Tool explicitly specifies sanitizer configuration for the tag, use it.
    +       * Otherwise, remove all attributes
    +       */
    +      result[tag.toLowerCase()] = this.toolsTags[tag].sanitizationConfig ?? {};
     
           return result;
         }, {});
    @@ -306,31 +318,69 @@ export default class Paste extends Module {
         }
       }
     
    +  /**
    +   * Get tags name list from either tag name or sanitization config.
    +   *
    +   * @param {string | object} tagOrSanitizeConfig - tag name or sanitize config object.
    +   * @returns {string[]} array of tags.
    +   */
    +  private collectTagNames(tagOrSanitizeConfig: string | SanitizerConfig): string[] {
    +    /**
    +     * If string, then it is a tag name.
    +     */
    +    if (_.isString(tagOrSanitizeConfig)) {
    +      return [ tagOrSanitizeConfig ];
    +    }
    +    /**
    +     * If object, then its keys are tags.
    +     */
    +    if (_.isObject(tagOrSanitizeConfig)) {
    +      return Object.keys(tagOrSanitizeConfig);
    +    }
    +
    +    /** Return empty tag list */
    +    return [];
    +  }
    +
       /**
        * Get tags to substitute by Tool
        *
        * @param tool - BlockTool object
        */
       private getTagsConfig(tool: BlockTool): void {
    -    const tags = tool.pasteConfig.tags || [];
    -
    -    tags.forEach((tag) => {
    -      if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
    -        _.log(
    -          `Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
    -          `because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
    -          'warn'
    -        );
    -
    -        return;
    -      }
    +    const tagsOrSanitizeConfigs = tool.pasteConfig.tags || [];
    +    const toolTags = [];
    +
    +    tagsOrSanitizeConfigs.forEach((tagOrSanitizeConfig) => {
    +      const tags = this.collectTagNames(tagOrSanitizeConfig);
    +
    +      /**
    +       * Add tags to toolTags array
    +       */
    +      toolTags.push(...tags);
    +      tags.forEach((tag) => {
    +        if (Object.prototype.hasOwnProperty.call(this.toolsTags, tag)) {
    +          _.log(
    +            `Paste handler for «${tool.name}» Tool on «${tag}» tag is skipped ` +
    +            `because it is already used by «${this.toolsTags[tag].tool.name}» Tool.`,
    +            'warn'
    +          );
    +
    +          return;
    +        }
    +        /**
    +         * Get sanitize config for tag.
    +         */
    +        const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;
     
    -      this.toolsTags[tag.toUpperCase()] = {
    -        tool,
    -      };
    +        this.toolsTags[tag.toUpperCase()] = {
    +          tool,
    +          sanitizationConfig,
    +        };
    +      });
         });
     
    -    this.tagsByTool[tool.name] = tags.map((t) => t.toUpperCase());
    +    this.tagsByTool[tool.name] = toolTags.map((t) => t.toUpperCase());
       }
     
       /**
    @@ -449,7 +499,7 @@ export default class Paste extends Module {
       private async processFiles(items: FileList): Promise<void> {
         const { BlockManager } = this.Editor;
     
    -    let dataToInsert: {type: string; event: PasteEvent}[];
    +    let dataToInsert: { type: string; event: PasteEvent }[];
     
         dataToInsert = await Promise.all(
           Array
    @@ -473,7 +523,7 @@ export default class Paste extends Module {
        *
        * @param {File} file - file to process
        */
    -  private async processFile(file: File): Promise<{event: PasteEvent; type: string}> {
    +  private async processFile(file: File): Promise<{ event: PasteEvent; type: string }> {
         const extension = _.getFileExtension(file);
     
         const foundConfig = Object
    @@ -515,6 +565,19 @@ export default class Paste extends Module {
        */
       private processHTML(innerHTML: string): PasteData[] {
         const { Tools } = this.Editor;
    +
    +    /**
    +     * @todo Research, do we really need to always wrap innerHTML to a div:
    +     *  - <img> tag could be processed separately, but for now it becomes div-wrapped
    +     *    and then .getNodes() returns strange: [document-fragment, img]
    +     *    (description of the method says that it should should return only block tags or fragments,
    +     *     but there are inline-block element along with redundant empty fragment)
    +     *  - probably this is a reason of bugs with unexpected new block creation instead of inline pasting:
    +     *      - https://github.com/codex-team/editor.js/issues/1427
    +     *      - https://github.com/codex-team/editor.js/issues/1244
    +     *      - https://github.com/codex-team/editor.js/issues/740
    +     *
    +     */
         const wrapper = $.make('DIV');
     
         wrapper.innerHTML = innerHTML;
    @@ -543,16 +606,65 @@ export default class Paste extends Module {
                 break;
             }
     
    -        const { tags } = tool.pasteConfig;
    +        const { tags: tagsOrSanitizeConfigs } = tool.pasteConfig;
     
    -        const toolTags = tags.reduce((result, tag) => {
    -          result[tag.toLowerCase()] = {};
    +        /**
    +         * Reduce the tags or sanitize configs to a single array of sanitize config.
    +         * For example:
    +         * If sanitize config is
    +         * [ 'tbody',
    +         *   {
    +         *     table: {
    +         *       width: true,
    +         *       height: true,
    +         *     },
    +         *   },
    +         *   {
    +         *      td: {
    +         *        colspan: true,
    +         *        rowspan: true,
    +         *      },
    +         *      tr: {  // <-- the second tag
    +         *        height: true,
    +         *      },
    +         *   },
    +         * ]
    +         * then sanitize config will be
    +         * [
    +         *  'table':{},
    +         *  'tbody':{width: true, height: true}
    +         *  'td':{colspan: true, rowspan: true},
    +         *  'tr':{height: true}
    +         * ]
    +         */
    +        const toolTags = tagsOrSanitizeConfigs.reduce((result, tagOrSanitizeConfig) => {
    +          const tags = this.collectTagNames(tagOrSanitizeConfig);
    +
    +          tags.forEach((tag) => {
    +            const sanitizationConfig = _.isObject(tagOrSanitizeConfig) ? tagOrSanitizeConfig[tag] : null;
    +
    +            result[tag] = sanitizationConfig || {};
    +          });
     
               return result;
             }, {});
    +
             const customConfig = Object.assign({}, toolTags, tool.baseSanitizeConfig);
     
    -        content.innerHTML = clean(content.innerHTML, customConfig);
    +        /**
    +         * A workaround for the HTMLJanitor bug with Tables (incorrect sanitizing of table.innerHTML)
    +         * https://github.com/guardian/html-janitor/issues/3
    +         */
    +        if (content.tagName.toLowerCase() === 'table') {
    +          const cleanTableHTML = clean(content.outerHTML, customConfig);
    +          const tmpWrapper = $.make('div', undefined, {
    +            innerHTML: cleanTableHTML,
    +          });
    +
    +          content = tmpWrapper.firstChild;
    +        } else {
    +          content.innerHTML = clean(content.innerHTML, customConfig);
    +        }
     
             const event = this.composePasteEvent('tag', {
               data: content,
    @@ -565,7 +677,12 @@ export default class Paste extends Module {
               event,
             };
           })
    -      .filter((data) => !$.isNodeEmpty(data.content) || $.isSingleTag(data.content));
    +      .filter((data) => {
    +        const isEmpty = $.isEmpty(data.content);
    +        const isSingleTag = $.isSingleTag(data.content);
    +
    +        return !isEmpty || isSingleTag;
    +      });
       }
     
       /**
    @@ -576,7 +693,7 @@ export default class Paste extends Module {
        * @returns {PasteData[]}
        */
       private processPlain(plain: string): PasteData[] {
    -    const { defaultBlock } = this.config as {defaultBlock: string};
    +    const { defaultBlock } = this.config as { defaultBlock: string };
     
         if (!plain) {
           return [];
    @@ -681,7 +798,7 @@ export default class Paste extends Module {
        *
        * @returns {Promise<{event: PasteEvent, tool: string}>}
        */
    -  private async processPattern(text: string): Promise<{event: PasteEvent; tool: string}> {
    +  private async processPattern(text: string): Promise<{ event: PasteEvent; tool: string }> {
         const pattern = this.toolsPatterns.find((substitute) => {
           const execResult = substitute.pattern.exec(text);
     
    @@ -878,3 +995,4 @@ export default class Paste extends Module {
         }) as PasteEvent;
       }
     }
    +
    
  • test/cypress/tests/api/tools.spec.ts+430 3 modified
    @@ -1,5 +1,5 @@
    -import { ToolboxConfig, BlockToolData, ToolboxConfigEntry } from '../../../../types';
    -import { TunesMenuConfig } from '../../../../types/tools';
    +import { ToolboxConfig, BlockToolData, ToolboxConfigEntry, PasteConfig } from '../../../../types';
    +import { HTMLPasteEvent, PasteEvent, TunesMenuConfig } from '../../../../types/tools';
     
     /* eslint-disable @typescript-eslint/no-empty-function */
     
    @@ -270,7 +270,7 @@ describe('Editor Tools Api', () => {
         });
       });
     
    -  context('Tunes', () => {
    +  context('Tunes — renderSettings()', () => {
         it('should contain a single block tune configured in tool\'s renderSettings() method', () => {
           /** Tool with single tunes menu entry configured */
           class TestTool {
    @@ -490,4 +490,431 @@ describe('Editor Tools Api', () => {
             .should('contain.text', sampleText);
         });
       });
    +
    +  /**
    +   * @todo cover all the pasteConfig properties
    +   */
    +  context('Paste — pasteConfig()', () => {
    +    context('tags', () => {
    +      /**
    +       * tags: ['H1', 'H2']
    +       */
    +      it('should use corresponding tool when the array of tag names specified', () => {
    +        /**
    +         * Test tool with pasteConfig.tags specified
    +         */
    +        class TestImgTool {
    +          /** config specified handled tag */
    +          public static get pasteConfig(): PasteConfig {
    +            return {
    +              tags: [ 'img' ], // only tag name specified. Attributes should be sanitized
    +            };
    +          }
    +
    +          /** onPaste callback will be stubbed below */
    +          public onPaste(): void {}
    +
    +          /** save is required for correct implementation of the BlockTool class */
    +          public save(): void {}
    +
    +          /** render is required for correct implementation of the BlockTool class */
    +          public render(): HTMLElement {
    +            return document.createElement('img');
    +          }
    +        }
    +
    +        const toolsOnPaste = cy.spy(TestImgTool.prototype, 'onPaste');
    +
    +        cy.createEditor({
    +          tools: {
    +            testTool: TestImgTool,
    +          },
    +        }).as('editorInstance');
    +
    +        cy.get('[data-cy=editorjs]')
    +          .get('div.ce-block')
    +          .click()
    +          .paste({
    +            'text/html': '<img>',
    +          })
    +          .then(() => {
    +            expect(toolsOnPaste).to.be.called;
    +          });
    +      });
    +
    +      /**
    +       * tags: ['img'] -> <img>
    +       */
    +      it('should sanitize all attributes from tag, if only tag name specified ', () => {
    +        /**
    +         * Variable used for spying the pasted element we are passing to the Tool
    +         */
    +        let pastedElement;
    +
    +        /**
    +         * Test tool with pasteConfig.tags specified
    +         */
    +        class TestImageTool {
    +          /** config specified handled tag */
    +          public static get pasteConfig(): PasteConfig {
    +            return {
    +              tags: [ 'img' ], // only tag name specified. Attributes should be sanitized
    +            };
    +          }
    +
    +          /** onPaste callback will be stubbed below */
    +          public onPaste(): void {}
    +
    +          /** save is required for correct implementation of the BlockTool class */
    +          public save(): void {}
    +
    +          /** render is required for correct implementation of the BlockTool class */
    +          public render(): HTMLElement {
    +            return document.createElement('img');
    +          }
    +        }
    +
    +        /**
    +         * Stub the onPaste method to access the PasteEvent data for assertion
    +         */
    +        cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
    +          pastedElement = event.detail.data;
    +        });
    +
    +        cy.createEditor({
    +          tools: {
    +            testImageTool: TestImageTool,
    +          },
    +        });
    +
    +        cy.get('[data-cy=editorjs]')
    +          .get('div.ce-block')
    +          .click()
    +          .paste({
    +            'text/html': '<img src="foo" onerror="alert(123)"/>', // all attributes should be sanitized
    +          })
    +          .then(() => {
    +            expect(pastedElement).not.to.be.undefined;
    +            expect(pastedElement.tagName.toLowerCase()).eq('img');
    +            expect(pastedElement.attributes.length).eq(0);
    +          });
    +      });
    +
    +      /**
    +       * tags: [{
    +       *   img: {
    +       *     src: true
    +       *   }
    +       * }]
    +       *   ->  <img src="">
    +       *
    +       */
    +      it('should leave attributes if entry specified as a sanitizer config ', () => {
    +        /**
    +         * Variable used for spying the pasted element we are passing to the Tool
    +         */
    +        let pastedElement;
    +
    +        /**
    +         * Test tool with pasteConfig.tags specified
    +         */
    +        class TestImageTool {
    +          /** config specified handled tag */
    +          public static get pasteConfig(): PasteConfig {
    +            return {
    +              tags: [
    +                {
    +                  img: {
    +                    src: true,
    +                  },
    +                },
    +              ],
    +            };
    +          }
    +
    +          /** onPaste callback will be stubbed below */
    +          public onPaste(): void {}
    +
    +          /** save is required for correct implementation of the BlockTool class */
    +          public save(): void {}
    +
    +          /** render is required for correct implementation of the BlockTool class */
    +          public render(): HTMLElement {
    +            return document.createElement('img');
    +          }
    +        }
    +
    +        /**
    +         * Stub the onPaste method to access the PasteEvent data for assertion
    +         */
    +        cy.stub(TestImageTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
    +          pastedElement = event.detail.data;
    +        });
    +
    +        cy.createEditor({
    +          tools: {
    +            testImageTool: TestImageTool,
    +          },
    +        });
    +
    +        cy.get('[data-cy=editorjs]')
    +          .get('div.ce-block')
    +          .click()
    +          .paste({
    +            'text/html': '<img src="foo" onerror="alert(123)"/>',
    +          })
    +          .then(() => {
    +            expect(pastedElement).not.to.be.undefined;
    +
    +            /**
    +             * Check that the <img> has only "src" attribute
    +             */
    +            expect(pastedElement.tagName.toLowerCase()).eq('img');
    +            expect(pastedElement.getAttribute('src')).eq('foo');
    +            expect(pastedElement.attributes.length).eq(1);
    +          });
    +      });
    +
    +      /**
    +       * tags: [
    +       *   'video',
    +       *   {
    +       *     source: {
    +       *       src: true
    +       *     }
    +       *   }
    +       * ]
    +       */
    +      it('should support mixed tag names and sanitizer config ', () => {
    +        /**
    +         * Variable used for spying the pasted element we are passing to the Tool
    +         */
    +        let pastedElement;
    +
    +        /**
    +         * Test tool with pasteConfig.tags specified
    +         */
    +        class TestTool {
    +          /** config specified handled tag */
    +          public static get pasteConfig(): PasteConfig {
    +            return {
    +              tags: [
    +                'video', // video should not have attributes
    +                {
    +                  source: { // source should have only src attribute
    +                    src: true,
    +                  },
    +                },
    +              ],
    +            };
    +          }
    +
    +          /** onPaste callback will be stubbed below */
    +          public onPaste(): void {}
    +
    +          /** save is required for correct implementation of the BlockTool class */
    +          public save(): void {}
    +
    +          /** render is required for correct implementation of the BlockTool class */
    +          public render(): HTMLElement {
    +            return document.createElement('tbody');
    +          }
    +        }
    +
    +        /**
    +         * Stub the onPaste method to access the PasteEvent data for assertion
    +         */
    +        cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
    +          pastedElement = event.detail.data;
    +        });
    +
    +        cy.createEditor({
    +          tools: {
    +            testTool: TestTool,
    +          },
    +        });
    +
    +        cy.get('[data-cy=editorjs]')
    +          .get('div.ce-block')
    +          .click()
    +          .paste({
    +            'text/html': '<video width="100"><source src="movie.mp4" type="video/mp4"></video>',
    +          })
    +          .then(() => {
    +            expect(pastedElement).not.to.be.undefined;
    +
    +            /**
    +             * Check that <video>  has no attributes
    +             */
    +            expect(pastedElement.tagName.toLowerCase()).eq('video');
    +            expect(pastedElement.attributes.length).eq(0);
    +
    +            /**
    +             * Check that the <source> has only 'src' attribute
    +             */
    +            expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');
    +            expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');
    +            expect(pastedElement.firstChild.attributes.length).eq(1);
    +          });
    +      });
    +
    +      /**
    +       * tags: [
    +       *   {
    +       *     td: { width: true },
    +       *     tr: { height: true }
    +       *   }
    +       * ]
    +       */
    +      it('should support config with several keys as the single entry', () => {
    +        /**
    +         * Variable used for spying the pasted element we are passing to the Tool
    +         */
    +        let pastedElement;
    +
    +        /**
    +         * Test tool with pasteConfig.tags specified
    +         */
    +        class TestTool {
    +          /** config specified handled tag */
    +          public static get pasteConfig(): PasteConfig {
    +            return {
    +              tags: [
    +                {
    +                  video: {
    +                    width: true,
    +                  },
    +                  source: {
    +                    src: true,
    +                  },
    +                },
    +              ],
    +            };
    +          }
    +
    +          /** onPaste callback will be stubbed below */
    +          public onPaste(): void {}
    +
    +          /** save is required for correct implementation of the BlockTool class */
    +          public save(): void {}
    +
    +          /** render is required for correct implementation of the BlockTool class */
    +          public render(): HTMLElement {
    +            return document.createElement('tbody');
    +          }
    +        }
    +
    +        /**
    +         * Stub the onPaste method to access the PasteEvent data for assertion
    +         */
    +        cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
    +          pastedElement = event.detail.data;
    +        });
    +
    +        cy.createEditor({
    +          tools: {
    +            testTool: TestTool,
    +          },
    +        });
    +
    +        cy.get('[data-cy=editorjs]')
    +          .get('div.ce-block')
    +          .click()
    +          .paste({
    +            'text/html': '<video width="100"><source src="movie.mp4" type="video/mp4"></video>',
    +          })
    +          .then(() => {
    +            expect(pastedElement).not.to.be.undefined;
    +            expect(pastedElement.tagName.toLowerCase()).eq('video');
    +
    +            /**
    +             * Check that the <tr> has the 'height' attribute
    +             */
    +            expect(pastedElement.firstChild.tagName.toLowerCase()).eq('source');
    +            expect(pastedElement.firstChild.getAttribute('src')).eq('movie.mp4');
    +          });
    +      });
    +
    +      /**
    +       * It covers a workaround HTMLJanitor bug with tables (incorrect sanitizing of table.innerHTML)
    +       * https://github.com/guardian/html-janitor/issues/3
    +       */
    +      it('should correctly sanitize Table structure (test for HTMLJanitor bug)', () => {
    +        /**
    +         * Variable used for spying the pasted element we are passing to the Tool
    +         */
    +        let pastedElement;
    +
    +        /**
    +         * Test tool with pasteConfig.tags specified
    +         */
    +        class TestTool {
    +          /** config specified handled tag */
    +          public static get pasteConfig(): PasteConfig {
    +            return {
    +              tags: [
    +                'table',
    +                'tbody',
    +                {
    +                  td: {
    +                    width: true,
    +                  },
    +                  tr: {
    +                    height: true,
    +                  },
    +                },
    +              ],
    +            };
    +          }
    +
    +          /** onPaste callback will be stubbed below */
    +          public onPaste(): void {}
    +
    +          /** save is required for correct implementation of the BlockTool class */
    +          public save(): void {}
    +
    +          /** render is required for correct implementation of the BlockTool class */
    +          public render(): HTMLElement {
    +            return document.createElement('tbody');
    +          }
    +        }
    +
    +        /**
    +         * Stub the onPaste method to access the PasteEvent data for assertion
    +         */
    +        cy.stub(TestTool.prototype, 'onPaste').callsFake((event: HTMLPasteEvent) => {
    +          pastedElement = event.detail.data;
    +        });
    +
    +        cy.createEditor({
    +          tools: {
    +            testTool: TestTool,
    +          },
    +        });
    +
    +        cy.get('[data-cy=editorjs]')
    +          .get('div.ce-block')
    +          .click()
    +          .paste({
    +            'text/html': '<table><tr height="50"><td width="300">Ho-Ho-Ho</td></tr></table>',
    +          })
    +          .then(() => {
    +            expect(pastedElement).not.to.be.undefined;
    +            expect(pastedElement.tagName.toLowerCase()).eq('table');
    +
    +            /**
    +             * Check that the <tr> has the 'height' attribute
    +             */
    +            expect(pastedElement.querySelector('tr')).not.to.be.undefined;
    +            expect(pastedElement.querySelector('tr').getAttribute('height')).eq('50');
    +
    +            /**
    +             * Check that the <td> has the 'width' attribute
    +             */
    +            expect(pastedElement.querySelector('td')).not.to.be.undefined;
    +            expect(pastedElement.querySelector('td').getAttribute('width')).eq('300');
    +          });
    +      });
    +    });
    +  });
     });
    
  • types/configs/paste-config.d.ts+12 2 modified
    @@ -1,12 +1,22 @@
    +import { SanitizerConfig } from "./sanitizer-config";
    +
     /**
      * Tool onPaste configuration object
      */
     export interface PasteConfig {
       /**
    -   * Array of tags Tool can substitute
    +   * Array of tags Tool can substitute.
    +   *
    +   * Could also contain a sanitize-config if you need to save some tag's attribute.
    +   * For example:
    +   * [
    +   *   {
    +   *     img: { src: true },
    +   *   }
    +   * ],
        * @type string[]
        */
    -  tags?: string[];
    +  tags?: (string | SanitizerConfig)[];
     
       /**
        * Object of string patterns Tool can substitute.
    
  • types/configs/sanitizer-config.d.ts+4 2 modified
    @@ -2,7 +2,9 @@
      * Sanitizer config of each HTML element
      * @see {@link https://github.com/guardian/html-janitor#options}
      */
    -type TagConfig = boolean | { [attr: string]: boolean | string };
    +export type TagConfig = boolean | { [attr: string]: boolean | string };
    +
    +export type SanitizerRule = TagConfig | ((el: Element) => TagConfig)
     
     export interface SanitizerConfig {
       /**
    @@ -37,5 +39,5 @@ export interface SanitizerConfig {
        *   }
        * }
        */
    -  [key: string]: TagConfig | ((el: Element) => TagConfig);
    +  [key: string]: SanitizerRule;
     }
    
  • types/index.d.ts+1 0 modified
    @@ -64,6 +64,7 @@ export {BlockTune, BlockTuneConstructable} from './block-tunes';
     export {
       EditorConfig,
       SanitizerConfig,
    +  SanitizerRule,
       PasteConfig,
       LogLevels,
       ConversionConfig,
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.