Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions content/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -49,7 +62,7 @@
return new window.DOMParser().parseFromString(markup, "text/html");
}

/**

Check warning on line 65 in content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js

View workflow job for this annotation

GitHub Actions / build-js (14)

Missing JSDoc return description
* Inner HTML of the first element child of the parsed document body (mirrors jQuery(html)[0].innerHTML for one root).
*
* @param {String} markup - HTML document string
Expand All @@ -64,7 +77,7 @@
return body.firstElementChild.innerHTML;
}

/**

Check warning on line 80 in content/src/content/jcr_root/apps/core/wcm/components/commons/editor/clientlibs/authoringutils/js/authoringMarkupUtils.js

View workflow job for this annotation

GitHub Actions / build-js (14)

Missing JSDoc return description
* Ensures a node can be inserted into targetDocument (uses importNode when the node comes from another document).
*
* @param {Node} node - element or fragment from parsing or another document
Expand All @@ -81,24 +94,134 @@
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
* @returns {Boolean}
* @param {*} value - attribute value (typically after DOM parsing, so entities are decoded)
* @returns {Boolean} true when the normalised value starts with javascript, data, or vbscript
*/
function linkValueHasExcludedRepositoryPrefix(value) {
if (value === undefined || value === null) {
return false;
}
var t = String(value).trim().toLowerCase();
var t = stripAsciiControlsAndWhitespaceForSchemeCheck(String(value)).toLowerCase();
return (
t.indexOf("javascript:") === 0 ||
t.indexOf("data:") === 0 ||
t.indexOf("vbscript:") === 0
);
}

/**
* 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})
*/
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 (AUTHORING_MARKUP_STRIPPED_ELEMENT_TAGS[tag]) {
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 === "action" ||
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 "";
Expand Down Expand Up @@ -236,7 +359,8 @@
innerHtmlFromFirstBodyChild: innerHtmlFromFirstBodyChild,
adoptNodeForDocument: adoptNodeForDocument,
linkValueHasExcludedRepositoryPrefix: linkValueHasExcludedRepositoryPrefix,
buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor
buildPageImageThumbnailShellForEditor: buildPageImageThumbnailShellForEditor,
sanitizeAuthoringEditorResponseMarkup: sanitizeAuthoringEditorResponseMarkup
};

})(window);
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[core.wcm.components.contentfragment.v1.dialog]"/>
categories="[core.wcm.components.contentfragment.v1.dialog]"
dependencies="[core.wcm.components.commons.editor.authoringutils]"/>
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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();
};

Expand Down Expand Up @@ -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);
65 changes: 65 additions & 0 deletions content/test/clientlibs/authoringutils/authoringMarkupUtilsTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -154,4 +170,53 @@ 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 =
"<html><body><div><span onmouseover=\"x\"><img src=\"x\" onerror=\"alert(1)\"></span></div></body></html>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("onerror")).toBe(-1);
expect(out.indexOf("onmouseover")).toBe(-1);
expect(out.indexOf("<img")).not.toBe(-1);
});

it("drops script tags from nested markup", function() {
const html = "<div><script>z</script><p>ok</p></div>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("<script>")).toBe(-1);
expect(out.indexOf(">ok</p>")).not.toBe(-1);
});

it("removes href with disallowed schemes", function() {
const html = "<div><a href=\"javascript:void(0)\">t</a></div>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("javascript")).toBe(-1);
});

it("removes action with disallowed schemes", function() {
const html = "<div><div action=\"javascript:void(0)\">t</div></div>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("javascript")).toBe(-1);
});

it("drops style, link, meta, base, and form elements", function() {
const html =
"<div><style>x</style><link rel=\"stylesheet\" href=\"/etc.clientlibs/a.css\">" +
"<meta http-equiv=\"refresh\" content=\"0\">" +
"<base href=\"http://example.com/\">" +
"<form action=\"/search\"></form><p>ok</p></div>";
const out = markupUtils.sanitizeAuthoringEditorResponseMarkup(html);
expect(out.indexOf("<style")).toBe(-1);
expect(out.indexOf("<link")).toBe(-1);
expect(out.indexOf("<meta")).toBe(-1);
expect(out.indexOf("<base")).toBe(-1);
expect(out.indexOf("<form")).toBe(-1);
expect(out.indexOf(">ok</p>")).not.toBe(-1);
});

it("returns empty string when body has no element child", function() {
expect(markupUtils.sanitizeAuthoringEditorResponseMarkup("")).toBe("");
});
});
});
Loading
Loading