From a0f40d23b181519a0d0e4d7dc0c65f3edf7243bd Mon Sep 17 00:00:00 2001 From: Yahor Chaptsou Date: Thu, 23 Apr 2026 19:55:40 +0200 Subject: [PATCH 1/6] [SITES-41323] improve cfv1 perfromance --- .../clientlibs/editor/dialog/js/editDialog.js | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js index ce52274332..77970a2486 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js @@ -62,6 +62,50 @@ var elementsController; + function pathExternalizesToCurrentHost(path) { + if (path === undefined || path === null) { + return false; + } + var str = String(path).trim(); + if (str.length === 0) { + return false; + } + var lowerStr = str.toLowerCase(); + if ( + lowerStr.indexOf("javascript:") === 0 || + lowerStr.indexOf("data:") === 0 || + lowerStr.indexOf("vbscript:") === 0 + ) { + return false; + } + var decoded; + try { + decoded = decodeURIComponent(str.split("+").join(" ")); + } catch (e) { + return false; + } + if (decoded.indexOf("..") !== -1) { + return false; + } + var external = Granite.HTTP.externalize(str); + var resolved; + try { + resolved = new URL(external, window.location.href); + } catch (ex) { + return false; + } + return resolved.origin === window.location.origin; + } + + function innerHtmlFromMarkupFirstWrapper(markup) { + var doc = new DOMParser().parseFromString(markup, "text/html"); + var body = doc.body; + if (!body || !body.firstElementChild) { + return ""; + } + return body.firstElementChild.innerHTML; + } + /** * A class which encapsulates the logic related to element selectors and variation name selector. */ @@ -148,6 +192,10 @@ if (typeof displayMode === "undefined") { displayMode = editDialog.querySelector(SELECTOR_DISPLAY_MODE_CHECKED).value; } + var pathForRequest = type === "variation" ? this.variationNamePath : this.elementsContainerPath; + if (!pathExternalizesToCurrentHost(pathForRequest)) { + return $.Deferred().reject().promise(); + } var data = { fragmentPath: fragmentPath.value, displayMode: displayMode @@ -305,7 +353,7 @@ * @param {String} html - outerHTML value for elementNamesContainer */ ElementsController.prototype._updateElementsHTML = function(html) { - this.elementNamesContainer.innerHTML = $(html)[0].innerHTML; + this.elementNamesContainer.innerHTML = innerHtmlFromMarkupFirstWrapper(html); this._updateFields(); }; @@ -318,14 +366,15 @@ */ ElementsController.prototype._updateElementsDOM = function(dom) { this._updateFields(); - if (dom.tagName === "CORAL-MULTIFIELD") { + var element = dom.ownerDocument !== document ? document.importNode(dom, true) : dom; + if (element.tagName === "CORAL-MULTIFIELD") { // replace the element names multifield's template - this.elementNames.template = dom.template; + this.elementNames.template = element.template; } else { this._clearValidationError(this.singleTextSelector); - dom.value = this.singleTextSelector.value; - this.singleTextSelector.parentNode.replaceChild(dom, this.singleTextSelector); - this.singleTextSelector = dom; + element.value = this.singleTextSelector.value; + this.singleTextSelector.parentNode.replaceChild(element, this.singleTextSelector); + this.singleTextSelector = element; this.singleTextSelector.removeAttribute("disabled"); } this._updateFields(); @@ -353,9 +402,10 @@ */ ElementsController.prototype._updateVariationDOM = function(dom) { // replace the variation name select, keeping its value - dom.value = this.variationName.value; - this.variationName.parentNode.replaceChild(dom, this.variationName); - this.variationName = dom; + var element = dom.ownerDocument !== document ? document.importNode(dom, true) : dom; + element.value = this.variationName.value; + this.variationName.parentNode.replaceChild(element, this.variationName); + this.variationName = element; this.variationName.removeAttribute("disabled"); this._updateFields(); }; From c8ec1f8b4bf871490b6da650d010795f9b8cb94c Mon Sep 17 00:00:00 2001 From: Yahor Chaptsou Date: Wed, 13 May 2026 13:02:16 +0200 Subject: [PATCH 2/6] [SITES-41323] cf v1 performance improve --- content/karma.conf.js | 4 +- .../authoringutils/js/authoringMarkupUtils.js | 80 ++++++++++++++++++- .../clientlibs/editor/dialog/.content.xml | 3 +- .../clientlibs/editor/dialog/js/editDialog.js | 77 +++++++++++++++++- .../authoringMarkupUtilsTest.js | 28 +++++++ .../contentfragment/editDialogTest.js | 80 +++++++++++++++++++ content/test/mocks.js | 2 + 7 files changed, 269 insertions(+), 5 deletions(-) diff --git a/content/karma.conf.js b/content/karma.conf.js index c291077dd6..132f7190d1 100644 --- a/content/karma.conf.js +++ b/content/karma.conf.js @@ -35,14 +35,14 @@ module.exports = function(config) { 'src/content/jcr_root/apps/core/wcm/components/image/v3/image/clientlibs/site/**/js/*.js', 'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/authoring/js/editAction.js', 'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/authoring/js/vcfRenderer.js', + 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/htmlidvalidator/js/*.js', + 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/*.js', 'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js', { pattern: 'src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/site/js/vcf.js', included: false, served: true }, - 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/htmlidvalidator/js/*.js', - 'src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/*.js', 'src/content/jcr_root/apps/core/wcm/components/image/v2/image/clientlibs/editor/js/image.js', 'src/content/jcr_root/apps/core/wcm/components/image/v3/image/clientlibs/editor/js/image.js', 'test/**/*Test.js', diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js index 18f2684f5f..ed29b5b1b3 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js @@ -99,6 +99,83 @@ ); } + /** + * Normalizes parsed authoring markup under a root element: drops disallowed subtrees and clears + * event-handler and disallowed URL schemes on link-like attributes. + * + * @param {Element} rootElement - parsed subtree root (typically {@code document.body}) + */ + function sanitizeAuthoringMarkupSubtree(rootElement) { + if (!rootElement || rootElement.nodeType !== 1) { + return; + } + var all = rootElement.querySelectorAll("*"); + var list = []; + var i; + for (i = 0; i < all.length; i++) { + list.push(all[i]); + } + list.push(rootElement); + var removeEls = []; + for (i = 0; i < list.length; i++) { + var el = list[i]; + if (el.nodeType !== 1) { + continue; + } + var tag = el.tagName; + if (tag === "SCRIPT" || tag === "IFRAME" || tag === "OBJECT" || tag === "EMBED") { + removeEls.push(el); + continue; + } + var attrs = el.attributes; + var names = []; + var j; + for (j = 0; attrs && j < attrs.length; j++) { + names.push(attrs[j].name); + } + for (j = 0; j < names.length; j++) { + var name = names[j]; + var val = el.getAttribute(name); + var nl = name.toLowerCase(); + if (/^on/i.test(name)) { + el.removeAttribute(name); + continue; + } + if ( + (nl === "href" || nl === "src" || nl === "formaction" || nl === "xlink:href") && + linkValueHasExcludedRepositoryPrefix(val) + ) { + el.removeAttribute(name); + } + } + } + for (i = 0; i < removeEls.length; i++) { + var node = removeEls[i]; + if (node.parentNode) { + node.parentNode.removeChild(node); + } + } + } + + /** + * Parses datasource HTML and returns the inner markup of the first body child, after subtree + * normalization is applied to the parsed document body. + * + * @param {String} markup - HTML document string from a datasource response + * @returns {String} normalized inner markup (empty string when the parsed body has no element child) + */ + function sanitizeAuthoringEditorResponseMarkup(markup) { + var doc = parseMarkupDocument(String(markup == null ? "" : markup)); + if (doc.body) { + sanitizeAuthoringMarkupSubtree(doc.body); + } + var body = doc.body; + if (!body || !body.firstElementChild) { + return ""; + } + return body.firstElementChild.innerHTML; + } + function filterClassAttribute(raw, allowedTokens) { if (!raw || typeof raw !== "string") { return ""; @@ -236,7 +313,8 @@ innerHtmlFromFirstBodyChild: innerHtmlFromFirstBodyChild, adoptNodeForDocument: adoptNodeForDocument, linkValueHasExcludedRepositoryPrefix: linkValueHasExcludedRepositoryPrefix, - buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor + buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor, + sanitizeAuthoringEditorResponseMarkup: sanitizeAuthoringEditorResponseMarkup }; })(window); diff --git a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml index 04103836f4..b2a5a45a8c 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml +++ b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/.content.xml @@ -1,4 +1,5 @@ + categories="[core.wcm.components.contentfragment.v1.dialog]" + dependencies="[core.wcm.components.commons.editor.authoringutils]"/> diff --git a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js index dfb4914b7b..2f1d9c67fb 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/contentfragment/v1/contentfragment/clientlibs/editor/dialog/js/editDialog.js @@ -49,6 +49,71 @@ // ui helper var ui = $(window).adaptTo("foundation-ui"); + var CT_SITES_41323 = "CT_SITES-41323"; + + /** + * Granite feature toggle CT_SITES-41323 gates datasource HTML handling for the element names container. + * When Granite or {@code Toggles} is unavailable, helpers are treated as enabled. + * When the toggle is explicitly disabled, behaviour matches earlier dialog revisions. + * + * @returns {Boolean} + */ + function isContentFragmentV1DialogAuthoringMarkupHelpersEnabled() { + if (typeof Granite === "undefined" || !Granite.Toggles || typeof Granite.Toggles.isEnabled !== "function") { + return true; + } + return Granite.Toggles.isEnabled(CT_SITES_41323) !== false; + } + + /** + * @returns {Object|null} {@code CQ.CoreComponents.AuthoringEditorUtils.markup} when present + */ + function getAuthoringMarkupUtils() { + if (window.CQ && window.CQ.CoreComponents && window.CQ.CoreComponents.AuthoringEditorUtils) { + return window.CQ.CoreComponents.AuthoringEditorUtils.markup; + } + return null; + } + + function extractFirstParsedInnerHtml(html) { + if (typeof $ === "function" && typeof $.parseHTML === "function") { + var nodes = $.parseHTML(String(html == null ? "" : html), document, true); + var i; + for (i = 0; i < nodes.length; i++) { + if (nodes[i].nodeType === 1) { + return nodes[i].innerHTML; + } + } + return ""; + } + var fallbackMarkup = getAuthoringMarkupUtils(); + if (fallbackMarkup && typeof fallbackMarkup.innerHtmlFromFirstBodyChild === "function") { + return fallbackMarkup.innerHtmlFromFirstBodyChild(html); + } + var first = $(html)[0]; + if (!first || typeof first.innerHTML !== "string") { + return ""; + } + return first.innerHTML; + } + + /** + * Resolves inner HTML for the element names container from a datasource response string. + * + * @param {String} html - outer HTML for the element names container + * @returns {String} + */ + function resolveElementNamesContainerInnerHtml(html) { + if (!isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()) { + return extractFirstParsedInnerHtml(html); + } + var markupUtils = getAuthoringMarkupUtils(); + if (markupUtils && typeof markupUtils.sanitizeAuthoringEditorResponseMarkup === "function") { + return markupUtils.sanitizeAuthoringEditorResponseMarkup(html); + } + return extractFirstParsedInnerHtml(html); + } + // dialog texts var confirmationDialogTitle = Granite.I18n.get("Warning"); var confirmationDialogMessage = Granite.I18n.get("Please confirm replacing the current content fragment and its configuration"); @@ -351,7 +416,7 @@ * @param {String} html - outerHTML value for elementNamesContainer */ ElementsController.prototype._updateElementsHTML = function(html) { - this.elementNamesContainer.innerHTML = $(html)[0].innerHTML; + this.elementNamesContainer.innerHTML = resolveElementNamesContainerInnerHtml(html); this._updateFields(); }; @@ -868,4 +933,14 @@ } }); + var cfV1DialogTestApiHost = typeof globalThis !== "undefined" ? globalThis : window; + /* Karma (mocks.js) sets __CONTENTFRAGMENT_V1_DIALOG_TEST_API on the global object; AEM runtime leaves it undefined. */ + if (cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API) { + cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.resolveElementNamesContainerInnerHtml = + resolveElementNamesContainerInnerHtml; + cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled = + isContentFragmentV1DialogAuthoringMarkupHelpersEnabled; + cfV1DialogTestApiHost.__CONTENTFRAGMENT_V1_DIALOG_TEST_API.getAuthoringMarkupUtils = getAuthoringMarkupUtils; + } + })(window, jQuery, jQuery(document), Granite, Coral); diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js index d25a020d00..9fedcc8785 100644 --- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js +++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js @@ -154,4 +154,32 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author expect(img.getAttribute("src")).toBe("/content/dam/x.png"); }); }); + + describe("sanitizeAuthoringEditorResponseMarkup", function() { + it("returns inner markup of the first body child with event attributes removed", function() { + const html = + "
"; + const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html); + expect(out.indexOf("onerror")).toBe(-1); + expect(out.indexOf("onmouseover")).toBe(-1); + expect(out.indexOf("")).toBe(-1); + expect(out.indexOf(">ok

")).not.toBe(-1); + }); + + it("removes href with disallowed schemes", function() { + const html = "
t
"; + const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html); + expect(out.indexOf("javascript")).toBe(-1); + }); + + it("returns empty string when body has no element child", function() { + expect(markupUtils.sanitizeAuthoringEditorResponseMarkup("")).toBe(""); + }); + }); }); diff --git a/content/test/clientlibs/contentfragment/editDialogTest.js b/content/test/clientlibs/contentfragment/editDialogTest.js index a1dae3c69f..9f3b8e28ed 100644 --- a/content/test/clientlibs/contentfragment/editDialogTest.js +++ b/content/test/clientlibs/contentfragment/editDialogTest.js @@ -13,6 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ +/** + * Helpers for {@code Granite.Toggles} during editDialog markup-helper test suites + * ({@code globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API}). + */ +function editDialogTest_graniteAllOn() { + globalThis.Granite.Toggles.isEnabled = function() { + return true; + }; +} + +function editDialogTest_graniteCt41323Off() { + globalThis.Granite.Toggles.isEnabled = function(key) { + return key !== "CT_SITES-41323"; + }; +} + describe("Test editDialog VCF template retention for", function() { let channel; @@ -167,3 +183,67 @@ describe("Test editDialog VCF template retention for", function() { }); }); + +/** + * Covers Content Fragment v1 editor {@code editDialog.js} (CT_SITES-41323) wired through + * {@code globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API}. Karma loads {@code authoringutils} before {@code editDialog.js}. + */ +describe("Content Fragment v1 editor editDialog.js (Karma-loaded)", function() { + let api; + let togglesIsEnabled; + + beforeAll(function() { + api = globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API; + togglesIsEnabled = globalThis.Granite.Toggles.isEnabled; + }); + + afterEach(function() { + globalThis.Granite.Toggles.isEnabled = togglesIsEnabled; + }); + + describe("__CONTENTFRAGMENT_V1_DIALOG_TEST_API", function() { + it("exposes resolve and toggle helpers", function() { + expect(api).toBeDefined(); + expect(typeof api.resolveElementNamesContainerInnerHtml).toBe("function"); + expect(typeof api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled).toBe("function"); + expect(typeof api.getAuthoringMarkupUtils).toBe("function"); + }); + }); + + describe("isContentFragmentV1DialogAuthoringMarkupHelpersEnabled", function() { + it("treats missing Granite.Toggles as enabled", function() { + const saved = globalThis.Granite.Toggles; + globalThis.Granite.Toggles = undefined; + expect(api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()).toBe(true); + globalThis.Granite.Toggles = saved; + }); + + it("returns false when CT_SITES-41323 is explicitly disabled", function() { + editDialogTest_graniteCt41323Off(); + expect(api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()).toBe(false); + }); + + it("returns true when CT_SITES-41323 is enabled", function() { + editDialogTest_graniteAllOn(); + expect(api.isContentFragmentV1DialogAuthoringMarkupHelpersEnabled()).toBe(true); + }); + }); + + describe("resolveElementNamesContainerInnerHtml", function() { + it("normalizes markup when helpers are enabled", function() { + editDialogTest_graniteAllOn(); + const doc = + "
"; + const inner = api.resolveElementNamesContainerInnerHtml(doc); + expect(inner.indexOf("onerror")).toBe(-1); + }); + + it("keeps legacy parsing when helpers are disabled", function() { + editDialogTest_graniteCt41323Off(); + const doc = + "
"; + const inner = api.resolveElementNamesContainerInnerHtml(doc); + expect(inner.indexOf("onerror")).not.toBe(-1); + }); + }); +}); diff --git a/content/test/mocks.js b/content/test/mocks.js index 9309133124..178d76f71d 100644 --- a/content/test/mocks.js +++ b/content/test/mocks.js @@ -427,3 +427,5 @@ if (!globalThis.CQ.CoreComponents.CheckboxTextfieldTuple) { /** Filled by Image v2 or v3 editor image.js when present (Karma loads those scripts after mocks). */ globalThis.__IMAGE_V2_EDITOR_TEST_API = {}; globalThis.__IMAGE_V3_EDITOR_TEST_API = {}; +/** Filled by Content Fragment v1 editDialog.js when present (Karma loads that script after mocks). */ +globalThis.__CONTENTFRAGMENT_V1_DIALOG_TEST_API = {}; From f1703a991a0b591bf3f888b97507c2e06c450c12 Mon Sep 17 00:00:00 2001 From: Yahor Chaptsou Date: Wed, 13 May 2026 13:54:05 +0200 Subject: [PATCH 3/6] [SITES-41323] cf v1 performance improve --- .../authoringutils/js/authoringMarkupUtils.js | 19 ++++++++++++++++--- .../authoringMarkupUtilsTest.js | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js index ed29b5b1b3..ecc14172c8 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js @@ -39,6 +39,19 @@ "data-thumbnail-config-path" ]; + /** Element names removed entirely when normalizing parsed authoring datasource markup. */ + var AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS = { + SCRIPT: true, + IFRAME: true, + OBJECT: true, + EMBED: true, + STYLE: true, + LINK: true, + META: true, + BASE: true, + FORM: true + }; + /** * Parses an HTML document string into a Document instance. * @@ -100,8 +113,8 @@ } /** - * Normalizes parsed authoring markup under a root element: drops disallowed subtrees and clears - * event-handler and disallowed URL schemes on link-like attributes. + * Normalizes parsed authoring markup under a root element: drops disallowed subtrees (including + * active content, document-influencing, and styling hooks) and clears event-handler and disallowed URL schemes on link-like attributes. * * @param {Element} rootElement - parsed subtree root (typically {@code document.body}) */ @@ -123,7 +136,7 @@ continue; } var tag = el.tagName; - if (tag === "SCRIPT" || tag === "IFRAME" || tag === "OBJECT" || tag === "EMBED") { + if (AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS[tag]) { removeEls.push(el); continue; } diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js index 9fedcc8785..790a621c55 100644 --- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js +++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js @@ -178,6 +178,21 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author expect(out.indexOf("javascript")).toBe(-1); }); + it("drops style, link, meta, base, and form elements", function() { + const html = + "
" + + "" + + "" + + "

ok

"; + const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html); + expect(out.indexOf("ok

")).not.toBe(-1); + }); + it("returns empty string when body has no element child", function() { expect(markupUtils.sanitizeAuthoringEditorResponseMarkup("")).toBe(""); }); From fd95fd6e32827436d40b9eee89e5e731c0b50c2b Mon Sep 17 00:00:00 2001 From: Yahor Chaptsou Date: Wed, 13 May 2026 14:00:09 +0200 Subject: [PATCH 4/6] [SITES-41323] cf v1 performance improve --- .../clientlibs/authoringutils/js/authoringMarkupUtils.js | 8 +++++++- .../clientlibs/authoringutils/authoringMarkupUtilsTest.js | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js index ecc14172c8..4adf8cc27f 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js @@ -155,7 +155,13 @@ continue; } if ( - (nl === "href" || nl === "src" || nl === "formaction" || nl === "xlink:href") && + ( + nl === "href" || + nl === "src" || + nl === "action" || + nl === "formaction" || + nl === "xlink:href" + ) && linkValueHasExcludedRepositoryPrefix(val) ) { el.removeAttribute(name); diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js index 790a621c55..59efe4b8ed 100644 --- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js +++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js @@ -178,6 +178,12 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author expect(out.indexOf("javascript")).toBe(-1); }); + it("removes action with disallowed schemes", function() { + const html = "
t
"; + const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html); + expect(out.indexOf("javascript")).toBe(-1); + }); + it("drops style, link, meta, base, and form elements", function() { const html = "
" + From fa1f046e595f231cf26820e1f274491ff4dca8e8 Mon Sep 17 00:00:00 2001 From: Yahor Chaptsou Date: Wed, 13 May 2026 14:15:15 +0200 Subject: [PATCH 5/6] [SITES-41323] cf v1 performance improve --- .../authoringutils/js/authoringMarkupUtils.js | 9 +++++++-- .../authoringutils/authoringMarkupUtilsTest.js | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js index 4adf8cc27f..aeae7500d0 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js @@ -96,15 +96,20 @@ /** * Whether a link-like attribute value uses a non-http(s) scheme prefix that authoring dialogs do not treat as repository paths. + * Leading C0 control characters, DEL, and whitespace are stripped before the check so values cannot hide schemes from prefix matching. * - * @param {*} value - attribute value + * @param {*} value - attribute value (typically after DOM parsing, so entities are decoded) * @returns {Boolean} */ function linkValueHasExcludedRepositoryPrefix(value) { if (value === undefined || value === null) { return false; } - var t = String(value).trim().toLowerCase(); + // Strip C0 controls (incl. NUL, TAB, LF, CR) and all whitespace so + // browser URL-parser normalisations like "java\tscript:" can't bypass + // the prefix check. Values reach this helper post-DOM-parse, so HTML + // entities are already decoded by the parser. + var t = String(value).replace(/[\u0000-\u001F\u007F\s]+/g, "").toLowerCase(); return ( t.indexOf("javascript:") === 0 || t.indexOf("data:") === 0 || diff --git a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js index 59efe4b8ed..1f1781ef61 100644 --- a/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js +++ b/content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js @@ -41,6 +41,22 @@ describe("AuthoringEditorUtils.markup (core.wcm.components.commons.editor.author expect(markupUtils.linkValueHasExcludedRepositoryPrefix("DATA:image/png;base64,xx")).toBe(true); expect(markupUtils.linkValueHasExcludedRepositoryPrefix(" javascript:x ")).toBe(true); }); + + it("returns true when C0 controls or whitespace break up or precede the scheme", function() { + expect(markupUtils.linkValueHasExcludedRepositoryPrefix(" javascript:alert(1)")).toBe(true); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\n\tjavascript:alert(1)")).toBe(true); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("java\tscript:alert(1)")).toBe(true); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("java\nscript:alert(1)")).toBe(true); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("jav\rascript:alert(1)")).toBe(true); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\u0000javascript:alert(1)")).toBe(true); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("\u0001javascript:alert(1)")).toBe(true); + }); + + it("returns false for https URLs and nullish values", function() { + expect(markupUtils.linkValueHasExcludedRepositoryPrefix("https://example.com/x")).toBe(false); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix(null)).toBe(false); + expect(markupUtils.linkValueHasExcludedRepositoryPrefix(undefined)).toBe(false); + }); }); describe("buildPageImageThumbnailShellForEditor", function() { From 63d898600c1fc71f33880a4b96ed279bbac57ca3 Mon Sep 17 00:00:00 2001 From: Yahor Chaptsou Date: Wed, 13 May 2026 14:26:19 +0200 Subject: [PATCH 6/6] [SITES-41323] cf v1 performance improve --- .../authoringutils/js/authoringMarkupUtils.js | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js index aeae7500d0..c132b3fc12 100644 --- a/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js +++ b/content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js @@ -94,22 +94,44 @@ return node; } + /** + * Drops ASCII C0 controls, DEL, and whitespace so scheme prefix checks cannot be bypassed with + * characters the URL layer may normalise away (e.g. TAB inside {@code javascript:}). + * + * @param {String} str - raw attribute value + * @returns {String} characters kept for scheme prefix checks + */ + function stripAsciiControlsAndWhitespaceForSchemeCheck(str) { + var out = ""; + var i; + var ch; + var c; + for (i = 0; i < str.length; i++) { + ch = str.charAt(i); + c = str.charCodeAt(i); + if (c <= 31 || c === 127) { + continue; + } + if (/\s/.test(ch)) { + continue; + } + out += ch; + } + return out; + } + /** * Whether a link-like attribute value uses a non-http(s) scheme prefix that authoring dialogs do not treat as repository paths. * Leading C0 control characters, DEL, and whitespace are stripped before the check so values cannot hide schemes from prefix matching. * * @param {*} value - attribute value (typically after DOM parsing, so entities are decoded) - * @returns {Boolean} + * @returns {Boolean} true when the normalised value starts with javascript, data, or vbscript */ function linkValueHasExcludedRepositoryPrefix(value) { if (value === undefined || value === null) { return false; } - // Strip C0 controls (incl. NUL, TAB, LF, CR) and all whitespace so - // browser URL-parser normalisations like "java\tscript:" can't bypass - // the prefix check. Values reach this helper post-DOM-parse, so HTML - // entities are already decoded by the parser. - var t = String(value).replace(/[\u0000-\u001F\u007F\s]+/g, "").toLowerCase(); + var t = stripAsciiControlsAndWhitespaceForSchemeCheck(String(value)).toLowerCase(); return ( t.indexOf("javascript:") === 0 || t.indexOf("data:") === 0 ||