Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- This WPT test may be referenced by the following Test IDs:
- /sanitizer-api/sanitizer-javascript-url.html - WPT Dashboard Interop Dashboard
<!DOCTYPE html>
<head>
<title>Testcases for handling javascript: URL attributes</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="support/html5lib-testcase-support.js"></script>
<script id="built-in-navigating-url-attributes-list" type="html5lib-testcases">
#data
<a href="javascript:alert(1)"></a>
#document
| <a>
#data
<area href="javascript:alert(1)"></area>
#document
| <area>
#data
<button formaction="javascript:alert(1)"></button>
#document
| <button>
#data
<form action="javascript:alert(1)"></form>
#document
| <form>
#data
<input formaction="javascript:alert(1)"></input>
#document
| <input>
#data
<svg><a href="javascript:alert(1)"></a></svg>
#document
| <svg svg>
| <svg a>
#data
<svg><a xlink:href="javascript:alert(1)"></a></svg>
#document
| <svg svg>
| <svg a>
</script>
<script id="mathml" type="html5lib-testcases">
#data
<math><mrow href="javascript:alert(1)"></mrow></math>
#document
| <math math>
| <math mrow>
#data
<math><msqrt href="javascript:alert(1)"></msqrt></math>
#document
| <math math>
| <math msqrt>
#data
<math><mtext href="javascript:alert(1)">Test</mtext></math>
#document
| <math math>
| <math mtext>
| "Test"
#data
<math><mrow xlink:href="javascript:alert(1)"></mrow></math>
#document
| <math math>
| <math mrow>
</script>
<script id="built-in-animating-url-attributes-list" type="html5lib-testcases">
#data
<svg><animate attributeName="href"></svg>
#document
| <svg svg>
| <svg animate>
#data
<svg><animate attributeName="xlink:href"></svg>
#document
| <svg svg>
| <svg animate>
#data
<svg><animateMotion attributeName="href"></svg>
#document
| <svg svg>
| <svg animateMotion>
| attributeName="href"
#data
<svg><animateMotion attributeName="xlink:href"></svg>
#document
| <svg svg>
| <svg animateMotion>
| attributeName="xlink:href"
#data
<svg><animateTransform attributeName="href"></svg>
#document
| <svg svg>
| <svg animateTransform>
#data
<svg><animateTransform attributeName="xlink:href"></svg>
#document
| <svg svg>
| <svg animateTransform>
#data
<svg><set attributeName="href"></svg>
#document
| <svg svg>
| <svg set>
#data
<svg><set attributeName="xlink:href"></svg>
#document
| <svg svg>
| <svg set>
</script>
<script id="allowed" type="html5lib-testcases">
#data
<a nothref="javascript:alert(1)"></a>
#document
| <a>
| nothref="javascript:alert(1)"
#data
<svg><a xlink:href="data:text/html,foobar"></a></svg>
#document
| <svg svg>
| <svg a>
| xlink href="data:text/html,foobar"
#data
<svg><set attributeName=" href "></svg>
#document
| <svg svg>
| <svg set>
| attributeName=" href "
</script>
<script id="url-normalization" type="html5lib-testcases">
#data
<a href="JaVaScRiPt:alert(1)"></a>
#document
| <a>
#data
<a href=" javascript:alert(1)"></a>
#document
| <a>
#data
<a href=" javascript:alert(1)"></a>
#document
| <a>
#data
<a href="java	script:alert(1)"></a>
#document
| <a>
#data
<a href="java script:alert(1)"></a>
#document
| <a>
#data
<a href="java script:alert(1)"></a>
#document
| <a>
#data
<a href="javascript:alert(1)"></a>
#document
| <a>
#data
<svg><a xlink:href="java	script:alert(1)"></a></svg>
#document
| <svg svg>
| <svg a>
</script>
<script>
for (const group of document.querySelectorAll("script[type='html5lib-testcases']")) {
parse_html5lib_testcases(group.textContent).forEach((testcase, index) => {
// 1. Safe methods: javascript URLs must always be removed regardless of config.
const safeConfigs = [
["{ sanitizer: {} }", { sanitizer: {} }],
["{ sanitizer: { javascriptURLs: false } }", { sanitizer: { javascriptURLs: false } }],
["{ sanitizer: { javascriptURLs: true } }", { sanitizer: { javascriptURLs: true } }],
["{ sanitizer: new Sanitizer({ javascriptURLs: true }) }", { sanitizer: new Sanitizer({ javascriptURLs: true }) }],
];
for (const [name, config] of safeConfigs) {
test((_) => {
const div = document.createElement("div");
div.setHTML(testcase.data, config);
assert_testcase(div, testcase);
}, `setHTML ${name} testcase ${group.id}/${index}, "${testcase.data}"`);
test((_) => {
assert_testcase(Document.parseHTML("<body>" + testcase.data, config).body, testcase);
}, `parseHTML ${name} testcase ${group.id}/${index}, "${testcase.data}"`);
}
// 2. Unsafe methods with javascriptURLs: false -> URLs must be removed (same as safe result).
// The default configuration has javascriptURLs: false as well, but its element and attribute
// allow lists remove nearly all the markup above, so it cannot be compared against these
// expectations. It is covered separately below.
const afterRemoveUnsafe = new Sanitizer({});
afterRemoveUnsafe.removeUnsafe();
const unsafeDisallowedConfigs = [
["{ sanitizer: { javascriptURLs: false } }", { sanitizer: { javascriptURLs: false } }],
["{ sanitizer: new Sanitizer({ javascriptURLs: false }) }", { sanitizer: new Sanitizer({ javascriptURLs: false }) }],
["{ sanitizer: new Sanitizer({}) } after removeUnsafe()", { sanitizer: afterRemoveUnsafe }],
];
for (const [name, config] of unsafeDisallowedConfigs) {
test((_) => {
const div = document.createElement("div");
div.setHTMLUnsafe(testcase.data, config);
assert_testcase(div, testcase);
}, `setHTMLUnsafe ${name} testcase ${group.id}/${index}, "${testcase.data}"`);
test((_) => {
assert_testcase(Document.parseHTMLUnsafe("<body>" + testcase.data, config).body, testcase);
}, `parseHTMLUnsafe ${name} testcase ${group.id}/${index}, "${testcase.data}"`);
}
});
}
// 3. Unsafe methods with the default configuration -> URLs must be removed.
//
// The default configuration has javascriptURLs: false, so the unsafe methods remove javascript:
// URLs as well. Its element and attribute allow lists remove nearly all the markup in the
// testcases above though, so it cannot be run against those expectations. HTML a/href and SVG
// a/href are the only pairs from the built-in navigating URL attributes list that the default
// configuration keeps at all; for every other pair the attribute is gone either way and
// javascriptURLs makes no observable difference.
const defaultConfigTestcases = [
{ selector: "a", markup: (url) => `<a href="${url}"></a>` },
{ selector: "svg a", markup: (url) => `<svg><a href="${url}"></a></svg>` },
];
const javascriptURL = "javascript:alert(1)";
function assert_href(root, selector, expected, description) {
const element = root.querySelector(selector);
assert_not_equals(element, null, `${description} (${selector} is kept)`);
assert_equals(element.getAttribute("href"), expected, description);
}
for (const { selector, markup } of defaultConfigTestcases) {
for (const [name, config] of [
[`{ sanitizer: "default" }`, { sanitizer: "default" }],
["{ sanitizer: new Sanitizer() }", { sanitizer: new Sanitizer() }],
]) {
test((_) => {
const div = document.createElement("div");
div.setHTMLUnsafe(markup(javascriptURL), config);
assert_href(div, selector, null, "javascript: URL is removed");
// href is allowed by the default configuration, so what removed it above is javascriptURLs
// and not the attribute allow list.
div.setHTMLUnsafe(markup(otherURL), config);
assert_href(div, selector, otherURL, "other URL is kept");
}, `setHTMLUnsafe ${name} default configuration, "${markup(javascriptURL)}"`);
test((_) => {
assert_href(Document.parseHTMLUnsafe("<body>" + markup(javascriptURL), config).body,
selector, null, "javascript: URL is removed");
assert_href(Document.parseHTMLUnsafe("<body>" + markup(otherURL), config).body,
selector, otherURL, "other URL is kept");
}, `parseHTMLUnsafe ${name} default configuration, "${markup(javascriptURL)}"`);
}
test((_) => {
const sanitizer = new Sanitizer();
assert_true(sanitizer.setJavascriptURLs(true), "javascriptURLs defaults to false");
const div = document.createElement("div");
div.setHTMLUnsafe(markup(javascriptURL), { sanitizer });
assert_href(div, selector, javascriptURL, "unsafe method keeps the javascript: URL");
div.setHTML(markup(javascriptURL), { sanitizer });
assert_href(div, selector, null, "safe method removes the javascript: URL anyway");
}, `setJavascriptURLs(true) on the default configuration, "${markup(javascriptURL)}"`);
}
// 4. Unsafe methods with javascriptURLs: true -> URLs must be preserved.
test(() => {
const unsafeAllowedConfigs = [
["{ sanitizer: {} }", { sanitizer: {} }],
["{ sanitizer: { javascriptURLs: true } }", { sanitizer: { javascriptURLs: true } }],
["{ sanitizer: new Sanitizer({ javascriptURLs: true }) }", { sanitizer: new Sanitizer({ javascriptURLs: true }) }],
["{ sanitizer: new Sanitizer({}) }", { sanitizer: new Sanitizer({}) }],
];
const probes = [
'<a href="javascript:alert(1)"></a>',
'<area href="javascript:alert(1)"></area>',
'<button formaction="javascript:alert(1)"></button>',
'<form action="javascript:alert(1)"></form>',
'<input formaction="javascript:alert(1)"></input>',
'<svg><a href="javascript:alert(1)"></a></svg>',
'<svg><a xlink:href="javascript:alert(1)"></a></svg>',
'<math><mrow href="javascript:alert(1)"></mrow></math>',
'<math><mrow xlink:href="javascript:alert(1)"></mrow></math>',
'<math><msqrt href="javascript:alert(1)"></msqrt></math>',
'<math><mtext href="javascript:alert(1)">Test</mtext></math>',
'<svg><animate attributeName="href"></svg>',
'<svg><animate attributeName="xlink:href"></svg>',
'<svg><animateMotion attributeName="href"></svg>',
'<svg><animateMotion attributeName="xlink:href"></svg>',
'<svg><animateTransform attributeName="href"></svg>',
'<svg><animateTransform attributeName="xlink:href"></svg>',
'<svg><set attributeName="href"></svg>',
'<svg><set attributeName="xlink:href"></svg>',
'<a href="JaVaScRiPt:alert(1)"></a>',
'<a href=" javascript:alert(1)"></a>',
'<a href=" javascript:alert(1)"></a>',
'<a href="java	script:alert(1)"></a>',
'<svg><a xlink:href="java	script:alert(1)"></a></svg>',
];
for (const [configName, config] of unsafeAllowedConfigs) {
for (const probe of probes) {
const div = document.createElement("div");
div.setHTMLUnsafe(probe, config);
const parsed = Document.parseHTMLUnsafe("<body>" + probe, config);
if (probe.includes("attributeName")) {
assert_true(div.querySelector("[attributeName]") !== null, `setHTMLUnsafe ${configName}: retains attributeName in ${probe}`);
assert_true(parsed.body.querySelector("[attributeName]") !== null, `parseHTMLUnsafe ${configName}: retains attributeName in ${probe}`);
} else if (probe.includes("formaction")) {
assert_true(div.querySelector("[formaction]") !== null, `setHTMLUnsafe ${configName}: retains formaction in ${probe}`);
assert_true(parsed.body.querySelector("[formaction]") !== null, `parseHTMLUnsafe ${configName}: retains formaction in ${probe}`);
} else if (probe.includes("action")) {
assert_true(div.querySelector("[action]") !== null, `setHTMLUnsafe ${configName}: retains action in ${probe}`);
assert_true(parsed.body.querySelector("[action]") !== null, `parseHTMLUnsafe ${configName}: retains action in ${probe}`);
} else if (probe.includes("xlink:href")) {
assert_true(div.querySelector("a, mrow")?.hasAttributeNS("http://www.w3.org/1999/xlink", "href") || div.querySelector("[*|href]") !== null, `setHTMLUnsafe ${configName}: retains xlink:href in ${probe}`);
assert_true(parsed.body.querySelector("a, mrow")?.hasAttributeNS("http://www.w3.org/1999/xlink", "href") || parsed.body.querySelector("[*|href]") !== null, `parseHTMLUnsafe ${configName}: retains xlink:href in ${probe}`);
} else {
assert_true(div.querySelector("[href]") !== null, `setHTMLUnsafe ${configName}: retains href in ${probe}`);
assert_true(parsed.body.querySelector("[href]") !== null, `parseHTMLUnsafe ${configName}: retains href in ${probe}`);
}
}
}
}, "javascript: URLs preserved in unsafe methods when javascriptURLs is true");
// 5. Modifier method setJavascriptURLs() effects on sanitization
test(() => {
let s = new Sanitizer();
s.setJavascriptURLs(true);
const div1 = document.createElement("div");
div1.setHTMLUnsafe('<a href="javascript:alert(1)"></a>', { sanitizer: s });
assert_true(div1.querySelector("a").hasAttribute("href"));
s.setJavascriptURLs(false);
const div2 = document.createElement("div");
div2.setHTMLUnsafe('<a href="javascript:alert(1)"></a>', { sanitizer: s });
assert_false(div2.querySelector("a").hasAttribute("href"));
}, "sanitizer.setJavascriptURLs() controls URL preservation in setHTMLUnsafe");
// 6. Template and ShadowRoot handling with javascriptURLs
test(() => {
const tplHTML = '<template><a href="javascript:alert(1)"></a></template>';
const divSafe = document.createElement("div");
divSafe.setHTML(tplHTML, { sanitizer: { javascriptURLs: true } });
assert_false(divSafe.querySelector("template").content.querySelector("a").hasAttribute("href"));
const divUnsafeFalse = document.createElement("div");
divUnsafeFalse.setHTMLUnsafe(tplHTML, { sanitizer: { javascriptURLs: false } });
assert_false(divUnsafeFalse.querySelector("template").content.querySelector("a").hasAttribute("href"));
const divUnsafeTrue = document.createElement("div");
divUnsafeTrue.setHTMLUnsafe(tplHTML, { sanitizer: { javascriptURLs: true } });
assert_true(divUnsafeTrue.querySelector("template").content.querySelector("a").hasAttribute("href"));
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
shadow.setHTMLUnsafe('<a href="javascript:alert(1)"></a>', { sanitizer: { javascriptURLs: false } });
assert_false(shadow.querySelector("a").hasAttribute("href"));
shadow.setHTMLUnsafe('<a href="javascript:alert(1)"></a>', { sanitizer: { javascriptURLs: true } });
assert_true(shadow.querySelector("a").hasAttribute("href"));
shadow.setHTML('<a href="javascript:alert(1)"></a>', { sanitizer: { javascriptURLs: true } });
assert_false(shadow.querySelector("a").hasAttribute("href"));
}, "Template and ShadowRoot handling with javascriptURLs");
</script>
</head>
<body>
</body>