diff --git a/packages/dom/src/serialize-frames.js b/packages/dom/src/serialize-frames.js index b34c3590b..06a650122 100644 --- a/packages/dom/src/serialize-frames.js +++ b/packages/dom/src/serialize-frames.js @@ -25,9 +25,17 @@ function getPolicy() { // Adds a `` element to the serialized iframe's ``. This is necessary when // embedded documents are serialized and their contents become root-relative. -function setBaseURI(dom) { +function setBaseURI(dom, warnings) { + let parsedURL; + try { + parsedURL = new URL(dom.baseURI); + } catch (e) { + /* istanbul ignore next */ + if (warnings) warnings.add(`Could not parse baseURI for iframe: ${dom.baseURI}`); + } + /* istanbul ignore if: sanity check */ - if (!new URL(dom.baseURI).hostname) return; + if (!parsedURL.hostname) return; let $base = document.createElement('base'); $base.href = dom.baseURI; @@ -57,7 +65,7 @@ export function serializeFrames({ dom, clone, warnings, resources, enableJavaScr // recersively serialize contents let serialized = serializeDOM({ - domTransformation: setBaseURI, + domTransformation: (dom) => setBaseURI(dom, warnings), dom: frame.contentDocument, enableJavaScript, disableShadowDOM diff --git a/packages/dom/test/serialize-frames.test.js b/packages/dom/test/serialize-frames.test.js index 0a9240649..824496545 100644 --- a/packages/dom/test/serialize-frames.test.js +++ b/packages/dom/test/serialize-frames.test.js @@ -147,6 +147,56 @@ describe('serializeFrames', () => { expect($('#frame-js-no-src')[0].getAttribute('srcdoc')).toBeNull(); }); + it(`${platform}: does not crash when an iframe has an unparseable baseURI`, () => { + // Simulate a transient/non-standard iframe baseURI (e.g. third-party widgets like Intercom) + // by creating an iframe whose contentDocument.baseURI is overridden to an invalid value. + let $frameInvalid = document.createElement('iframe'); + $frameInvalid.id = 'frame-invalid-base-uri'; + document.getElementById('test').appendChild($frameInvalid); + + // Wait for it to be accessible, then stub the baseURI + let doc = $frameInvalid.contentDocument; + if (doc) { + Object.defineProperty(doc, 'baseURI', { value: 'not-a-valid-url', configurable: true }); + } + + // Should not throw, and the frame should simply not get a tag + let result; + expect(() => { result = serializeDOM(); }).not.toThrow(); + + let $parsed = parseDOM(result.html, platform); + // The invalid-base-uri frame should still be present (just without a injected) + expect($parsed('#frame-invalid-base-uri')).toBeDefined(); + + $frameInvalid.remove(); + }); + + it(`${platform}: handles catch block when URL constructor throws`, () => { + // Create an iframe that will trigger the catch block in setBaseURI + let $frameURLError = document.createElement('iframe'); + $frameURLError.id = 'frame-url-error'; + document.getElementById('test').appendChild($frameURLError); + + // Mock the baseURI to return a value that will cause URL constructor to throw + let doc = $frameURLError.contentDocument; + if (doc) { + Object.defineProperty(doc, 'baseURI', { + value: 'ht!tp://invalid url with spaces', + configurable: true + }); + } + + // Should not throw and should handle the error gracefully + let result; + expect(() => { result = serializeDOM(); }).not.toThrow(); + + let $parsed = parseDOM(result.html, platform); + // The frame should be present but without a tag due to the URL error + expect($parsed('#frame-url-error')).toBeDefined(); + + $frameURLError.remove(); + }); + it(`${platform}: does not serialize iframes without document elements`, () => { expect($('#frame-empty')[0]).toBeDefined(); expect($('#frame-empty')[0].getAttribute('srcdoc')).toBe(''); @@ -187,6 +237,96 @@ describe('serializeFrames', () => { } } }); + + it('handles createPolicy throwing an error gracefully', () => { + let createPolicy = jasmine.createSpy('createPolicy').and.throwError('Policy creation not allowed'); + let trustedTypesDescriptor = Object.getOwnPropertyDescriptor(window, 'trustedTypes'); + + // Reset policy to ensure we don't use a cached version + resetPolicy(); + + Object.defineProperty(window, 'trustedTypes', { + value: { createPolicy }, + configurable: true + }); + + try { + let result = serializeDOM(); + expect(createPolicy).toHaveBeenCalled(); + expect(result.html).toBeTruthy(); + } finally { + if (trustedTypesDescriptor) { + Object.defineProperty(window, 'trustedTypes', trustedTypesDescriptor); + } else { + delete window.trustedTypes; + } + } + }); + + it('handles setAttribute throwing an error when setting srcdoc', async () => { + await getFrame('frame-input'); + let originalSetAttribute = window.HTMLIFrameElement.prototype.setAttribute; + let setAttributeCalled = false; + + spyOn(window.HTMLIFrameElement.prototype, 'setAttribute').and.callFake(function(name, value) { + if (name === 'srcdoc') { + setAttributeCalled = true; + throw new Error('setAttribute not allowed'); + } + return originalSetAttribute.call(this, name, value); + }); + + try { + let result = serializeDOM(); + expect(setAttributeCalled).toBe(true); + expect(result.html).toBeTruthy(); + expect(result.html).toContain('frame-input'); + } finally { + window.HTMLIFrameElement.prototype.setAttribute = originalSetAttribute; + } + }); + + it('handles missing trustedTypes gracefully', () => { + let trustedTypesDescriptor = Object.getOwnPropertyDescriptor(window, 'trustedTypes'); + + // Reset policy to ensure we don't use a cached version + resetPolicy(); + + // Remove trustedTypes entirely + delete window.trustedTypes; + + try { + let result = serializeDOM(); + expect(result.html).toBeTruthy(); + } finally { + if (trustedTypesDescriptor) { + Object.defineProperty(window, 'trustedTypes', trustedTypesDescriptor); + } + } + }); + + it('handles trustedTypes without createPolicy method', () => { + let trustedTypesDescriptor = Object.getOwnPropertyDescriptor(window, 'trustedTypes'); + + // Reset policy to ensure we don't use a cached version + resetPolicy(); + + Object.defineProperty(window, 'trustedTypes', { + value: {}, // trustedTypes exists but without createPolicy + configurable: true + }); + + try { + let result = serializeDOM(); + expect(result.html).toBeTruthy(); + } finally { + if (trustedTypesDescriptor) { + Object.defineProperty(window, 'trustedTypes', trustedTypesDescriptor); + } else { + delete window.trustedTypes; + } + } + }); } }); });