diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2038b02..c822d9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
# Changelog
+- Protocol-relative URLs are properly supported for script tags
+
## 2.7.0 (2022-02-04)
- Allows a more sensible set of default attributes on `` tags. Thanks to [Zade Viggers](https://github.com/zadeviggers).
diff --git a/index.js b/index.js
index 8b21565..754bf10 100644
--- a/index.js
+++ b/index.js
@@ -287,7 +287,6 @@ function sanitizeHtml(html, options, _recursing) {
delete frame.attribs[a];
return;
}
- let parsed;
// check allowedAttributesMap for the element and attribute and modify the value
// as necessary if there are specific values defined.
let passedAllowedAttributesMapCheck = false;
@@ -335,14 +334,14 @@ function sanitizeHtml(html, options, _recursing) {
let allowed = true;
try {
- const parsed = new URL(value);
+ const parsed = parseUrl(value);
if (options.allowedScriptHostnames || options.allowedScriptDomains) {
const allowedHostname = (options.allowedScriptHostnames || []).find(function (hostname) {
- return hostname === parsed.hostname;
+ return hostname === parsed.url.hostname;
});
const allowedDomain = (options.allowedScriptDomains || []).find(function(domain) {
- return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
+ return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
});
allowed = allowedHostname || allowedDomain;
}
@@ -359,29 +358,9 @@ function sanitizeHtml(html, options, _recursing) {
if (name === 'iframe' && a === 'src') {
let allowed = true;
try {
- // Chrome accepts \ as a substitute for / in the // at the
- // start of a URL, so rewrite accordingly to prevent exploit.
- // Also drop any whitespace at that point in the URL
- value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//');
- if (value.startsWith('relative:')) {
- // An attempt to exploit our workaround for base URLs being
- // mandatory for relative URL validation in the WHATWG
- // URL parser, reject it
- throw new Error('relative: exploit attempt');
- }
- // naughtyHref is in charge of whether protocol relative URLs
- // are cool. Here we are concerned just with allowed hostnames and
- // whether to allow relative URLs.
- //
- // Build a placeholder "base URL" against which any reasonable
- // relative URL may be parsed successfully
- let base = 'relative://relative-site';
- for (let i = 0; (i < 100); i++) {
- base += `/${i}`;
- }
- const parsed = new URL(value, base);
- const isRelativeUrl = parsed && parsed.hostname === 'relative-site' && parsed.protocol === 'relative:';
- if (isRelativeUrl) {
+ const parsed = parseUrl(value);
+
+ if (parsed.isRelativeUrl) {
// default value of allowIframeRelativeUrls is true
// unless allowedIframeHostnames or allowedIframeDomains specified
allowed = has(options, 'allowIframeRelativeUrls')
@@ -389,10 +368,10 @@ function sanitizeHtml(html, options, _recursing) {
: (!options.allowedIframeHostnames && !options.allowedIframeDomains);
} else if (options.allowedIframeHostnames || options.allowedIframeDomains) {
const allowedHostname = (options.allowedIframeHostnames || []).find(function (hostname) {
- return hostname === parsed.hostname;
+ return hostname === parsed.url.hostname;
});
const allowedDomain = (options.allowedIframeDomains || []).find(function(domain) {
- return parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
+ return parsed.url.hostname === domain || parsed.url.hostname.endsWith(`.${domain}`);
});
allowed = allowedHostname || allowedDomain;
}
@@ -407,7 +386,7 @@ function sanitizeHtml(html, options, _recursing) {
}
if (a === 'srcset') {
try {
- parsed = parseSrcset(value);
+ let parsed = parseSrcset(value);
parsed.forEach(function(value) {
if (naughtyHref('srcset', value.url)) {
value.evil = true;
@@ -656,6 +635,33 @@ function sanitizeHtml(html, options, _recursing) {
return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1;
}
+ function parseUrl(value) {
+ value = value.replace(/^(\w+:)?\s*[\\/]\s*[\\/]/, '$1//');
+ if (value.startsWith('relative:')) {
+ // An attempt to exploit our workaround for base URLs being
+ // mandatory for relative URL validation in the WHATWG
+ // URL parser, reject it
+ throw new Error('relative: exploit attempt');
+ }
+ // naughtyHref is in charge of whether protocol relative URLs
+ // are cool. Here we are concerned just with allowed hostnames and
+ // whether to allow relative URLs.
+ //
+ // Build a placeholder "base URL" against which any reasonable
+ // relative URL may be parsed successfully
+ let base = 'relative://relative-site';
+ for (let i = 0; (i < 100); i++) {
+ base += `/${i}`;
+ }
+
+ const parsed = new URL(value, base);
+
+ const isRelativeUrl = parsed && parsed.hostname === 'relative-site' && parsed.protocol === 'relative:';
+ return {
+ isRelativeUrl,
+ url: parsed
+ };
+ }
/**
* Filters user input css properties by allowlisted regex attributes.
* Modifies the abstractSyntaxTree object.
diff --git a/test/test.js b/test/test.js
index 868b3ef..a81ea9c 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1476,5 +1476,16 @@ describe('sanitizeHtml', function() {
}), ''
);
});
+ it('Should allow protocol-relative URLs for script tag', function() {
+ assert.equal(
+ sanitizeHtml('', {
+ allowedTags: [ 'script' ],
+ allowedAttributes: {
+ script: [ 'src' ]
+ },
+ allowIframeRelativeUrls: true
+ }), ''
+ );
+ });
});