Browse Source

Merge pull request from GHSA-gm68-572p-q28r

* fix(admin-ui): Prevent XSS in rich text editor

* fix(admin-ui): Prevent XSS in iframe srcdoc in rich text editor
Michael Bromley 2 years ago
parent
commit
0cdc92b241

+ 3 - 2
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/custom-nodes.ts

@@ -29,9 +29,10 @@ export const iframeNode: NodeSpec = {
                         name: node.name,
                         name: node.name,
                         referrerpolicy: node.referrerPolicy,
                         referrerpolicy: node.referrerPolicy,
                         src: node.src,
                         src: node.src,
-                        srcdoc: node.srcdoc || undefined,
                         title: node.title ?? '',
                         title: node.title ?? '',
                         width: node.width,
                         width: node.width,
+                        // Note: we do not allow the `srcdoc` attribute to be
+                        // set as it presents an XSS attack vector
                     };
                     };
                     if (node.sandbox.length) {
                     if (node.sandbox.length) {
                         attrs.sandbox = node.sandbox;
                         attrs.sandbox = node.sandbox;
@@ -43,7 +44,7 @@ export const iframeNode: NodeSpec = {
         },
         },
     ],
     ],
     toDOM(node) {
     toDOM(node) {
-        return ['iframe', { ...node.attrs }];
+        return ['iframe', { ...node.attrs, sandbox: 'allow-scripts allow-same-origin' }];
     },
     },
 };
 };
 
 

+ 16 - 2
packages/admin-ui/src/lib/core/src/shared/components/rich-text-editor/prosemirror/prosemirror.service.ts

@@ -44,6 +44,11 @@ export class ProsemirrorService {
         marks: schema.spec.marks,
         marks: schema.spec.marks,
     });
     });
     private enabled = true;
     private enabled = true;
+    /**
+     * This is a Document used for processing incoming text. It ensures that malicious HTML is not executed by the
+     * actual document that is attached to the browser DOM, which could cause XSS attacks.
+     */
+    private detachedDoc: Document | null = null;
 
 
     constructor(private injector: Injector, private contextMenuService: ContextMenuService) {}
     constructor(private injector: Injector, private contextMenuService: ContextMenuService) {}
 
 
@@ -110,7 +115,8 @@ export class ProsemirrorService {
     }
     }
 
 
     private getStateFromText(text: string | null | undefined): EditorState {
     private getStateFromText(text: string | null | undefined): EditorState {
-        const div = document.createElement('div');
+        const doc = this.getDetachedDoc();
+        const div = doc.createElement('div');
         div.innerHTML = text ?? '';
         div.innerHTML = text ?? '';
         return EditorState.create({
         return EditorState.create({
             doc: DOMParser.fromSchema(this.mySchema).parse(div),
             doc: DOMParser.fromSchema(this.mySchema).parse(div),
@@ -119,7 +125,8 @@ export class ProsemirrorService {
     }
     }
 
 
     private getTextFromState(state: EditorState): string {
     private getTextFromState(state: EditorState): string {
-        const div = document.createElement('div');
+        const doc = this.getDetachedDoc();
+        const div = doc.createElement('div');
         const fragment = DOMSerializer.fromSchema(this.mySchema).serializeFragment(state.doc.content);
         const fragment = DOMSerializer.fromSchema(this.mySchema).serializeFragment(state.doc.content);
 
 
         div.appendChild(fragment);
         div.appendChild(fragment);
@@ -158,4 +165,11 @@ export class ProsemirrorService {
             }),
             }),
         );
         );
     }
     }
+
+    private getDetachedDoc() {
+        if (!this.detachedDoc) {
+            this.detachedDoc = document.implementation.createHTMLDocument();
+        }
+        return this.detachedDoc;
+    }
 }
 }