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..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
@@ -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.
*
@@ -81,17 +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
- * @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 ||
@@ -99,6 +139,89 @@
);
}
+ /**
+ * 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 "";
@@ -236,7 +359,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 @@
ok