diff options
Diffstat (limited to 'testing/web-platform/tests/fetch/security')
6 files changed, 580 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/security/1xx-response.any.js b/testing/web-platform/tests/fetch/security/1xx-response.any.js new file mode 100644 index 0000000000..df4dafcd80 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/1xx-response.any.js @@ -0,0 +1,28 @@ +promise_test(async (t) => { + // The 100 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(100)')); +}, 'Status(100) should be ignored.'); + +// This behavior is being discussed at https://github.com/whatwg/fetch/issues/1397. +promise_test(async (t) => { + const res = await fetch('/common/text-plain.txt?pipe=status(101)'); + assert_equals(res.status, 101); + const body = await res.text(); + assert_equals(body, ''); +}, 'Status(101) should be accepted, with removing body.'); + +promise_test(async (t) => { + // The 103 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(103)')); +}, 'Status(103) should be ignored.'); + +promise_test(async (t) => { + // The 199 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(199)')); +}, 'Status(199) should be ignored.'); diff --git a/testing/web-platform/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html b/testing/web-platform/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html new file mode 100644 index 0000000000..f27735daa1 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html @@ -0,0 +1,229 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function readableURL(url) { + return url.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"); + } + + // For each of the following tests, we'll inject a frame containing the HTML we'd like to poke at + // as a `srcdoc` attribute. Because we're injecting markup via `srcdoc`, we need to entity-escape + // the content we'd like to treat as "raw" (e.g. `\n` => ` `, `<` => `<`), and + // double-escape the "escaped" content. + var rawBrace = "<"; + var escapedBrace = "&lt;"; + var doubleEscapedBrace = "&amp;lt;"; + var rawNewline = " "; + var escapedNewline = "&#10;"; + // doubleEscapedNewline is used inside a data URI, and so must have its '#' escaped. + var doubleEscapedNewline = "&amp;%2310;"; + + function appendFrameAndGetElement(test, frame) { + return new Promise((resolve, reject) => { + frame.onload = test.step_func(_ => { + frame.onload = null; + resolve(frame.contentDocument.querySelector('#dangling')); + }); + document.body.appendChild(frame); + }); + } + + function assert_img_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 1, "Height"); + frame.remove(); + })); + } + + function assert_img_not_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 0, "Height"); + assert_equals(img.naturalWidth, 0, "Width"); + })); + } + + function assert_nested_img_not_loaded(test, frame) { + window.addEventListener('message', test.step_func(e => { + if (e.source != frame.contentWindow) + return; + + assert_equals(e.data, 'error'); + test.done(); + })); + appendFrameAndGetElement(test, frame); + } + + function assert_nested_img_loaded(test, frame) { + window.addEventListener('message', test.step_func(e => { + if (e.source != frame.contentWindow) + return; + + assert_equals(e.data, 'loaded'); + test.done(); + })); + appendFrameAndGetElement(test, frame); + } + + function createFrame(markup) { + var i = document.createElement('iframe'); + i.srcdoc = `${markup}sekrit`; + return i; + } + + // Subresource requests: + [ + // Data URLs don't themselves trigger blocking: + `<img id="dangling" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`, + `<img id="dangling" src="data:image/png;base64,${rawNewline}iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`, + `<img id="dangling" src="data:image/png;base64,i${rawNewline}VBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">`, + + // Data URLs with visual structure don't trigger blocking + `<img id="dangling" src="data:image/svg+xml;utf8, + <svg width='1' height='1' xmlns='http://www.w3.org/2000/svg'> + <rect width='100%' height='100%' fill='rebeccapurple'/> + <rect x='10%' y='10%' width='80%' height='80%' fill='lightgreen'/> + </svg>">` + ].forEach(markup => { + async_test(t => { + var i = createFrame(`${markup} <element attr="" another=''>`); + assert_img_loaded(t, i); + }, readableURL(markup)); + }); + + // Nested subresource requests: + // + // The following tests load a given HTML string into `<iframe srcdoc="...">`, so we'll + // end up with a frame with an ID of `dangling` inside the srcdoc frame. That frame's + // `src` is a `data:` URL that resolves to an HTML document containing an `<img>`. The + // error/load handlers on that image are piped back up to the top-level document to + // determine whether the tests' expectations were met. *phew* + + // Allowed: + [ + // Just a newline: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png'> + "> + </iframe>`, + + // Just a brace: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/green-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Newline and escaped brace. + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${doubleEscapedBrace}'> + "> + </iframe>`, + + // Brace and escaped newline: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/green-256x256.png?${doubleEscapedNewline}${rawBrace}'> + "> + </iframe>`, + ].forEach(markup => { + async_test(t => { + var i = createFrame(` + <script> + // Repeat the message so that the parent can track this frame as the source. + window.onmessage = e => window.parent.postMessage(e.data, '*'); + </scr`+`ipt> + ${markup} + `); + assert_nested_img_loaded(t, i); + }, readableURL(markup)); + }); + + // Nested requests that should fail: + [ + // Newline and brace: + `<iframe id="dangling" + src="data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading whitespace: + `<iframe id="dangling" + src=" data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading newline: + `<iframe id="dangling" + src="\ndata:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + `<iframe id="dangling" + src="${rawNewline}data:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading tab: + `<iframe id="dangling" + src="\tdata:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + + // Leading carrige return: + `<iframe id="dangling" + src="\rdata:text/html, + <img + onload='window.parent.postMessage("loaded", "*");' + onerror='window.parent.postMessage("error", "*");' + src='http://{{host}}:{{ports[http][0]}}/images/gr${rawNewline}een-256x256.png?${rawBrace}'> + "> + </iframe>`, + ].forEach(markup => { + async_test(t => { + var i = createFrame(` + <script> + // Repeat the message so that the parent can track this frame as the source. + window.onmessage = e => window.parent.postMessage(e.data, '*'); + </scr`+`ipt> + ${markup} + `); + assert_nested_img_not_loaded(t, i); + }, readableURL(markup)); + }); +</script> diff --git a/testing/web-platform/tests/fetch/security/dangling-markup-mitigation.tentative.html b/testing/web-platform/tests/fetch/security/dangling-markup-mitigation.tentative.html new file mode 100644 index 0000000000..61a931608b --- /dev/null +++ b/testing/web-platform/tests/fetch/security/dangling-markup-mitigation.tentative.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + function readableURL(url) { + return url.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t"); + } + + var should_load = [ + `/images/green-1x1.png`, + `/images/gre\nen-1x1.png`, + `/images/gre\ten-1x1.png`, + `/images/gre\ren-1x1.png`, + `/images/green-1x1.png?img=<`, + `/images/green-1x1.png?img=<`, + `/images/green-1x1.png?img=%3C`, + `/images/gr\neen-1x1.png?img=%3C`, + `/images/gr\reen-1x1.png?img=%3C`, + `/images/gr\teen-1x1.png?img=%3C`, + `/images/green-1x1.png?img= `, + `/images/gr\neen-1x1.png?img= `, + `/images/gr\reen-1x1.png?img= `, + `/images/gr\teen-1x1.png?img= `, + ]; + should_load.forEach(url => async_test(t => { + fetch(url) + .then(t.step_func_done(r => { + assert_equals(r.status, 200); + })) + .catch(t.unreached_func("Fetch should succeed.")); + }, "Fetch: " + readableURL(url))); + + var should_block = [ + `/images/gre\nen-1x1.png?img=<`, + `/images/gre\ren-1x1.png?img=<`, + `/images/gre\ten-1x1.png?img=<`, + `/images/green-1x1.png?<\n=block`, + `/images/green-1x1.png?<\r=block`, + `/images/green-1x1.png?<\t=block`, + ]; + should_block.forEach(url => async_test(t => { + fetch(url) + .then(t.unreached_func("Fetch should fail.")) + .catch(t.step_func_done()); + }, "Fetch: " + readableURL(url))); + + + // For each of the following tests, we'll inject a frame containing the HTML we'd like to poke at + // as a `srcdoc` attribute. Because we're injecting markup via `srcdoc`, we need to entity-escape + // the content we'd like to treat as "raw" (e.g. `\n` => ` `, `<` => `<`), and + // double-escape the "escaped" content. + var rawBrace = "<"; + var escapedBrace = "&lt;"; + var rawNewline = " "; + var escapedNewline = "&#10;"; + + function appendFrameAndGetElement(test, frame) { + return new Promise((resolve, reject) => { + frame.onload = test.step_func(_ => { + frame.onload = null; + resolve(frame.contentDocument.querySelector('#dangling')); + }); + document.body.appendChild(frame); + }); + } + + function assert_img_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 1, "Height"); + frame.remove(); + })); + } + + function assert_img_not_loaded(test, frame) { + appendFrameAndGetElement(test, frame) + .then(test.step_func_done(img => { + assert_equals(img.naturalHeight, 0, "Height"); + assert_equals(img.naturalWidth, 0, "Width"); + })); + } + + function createFrame(markup) { + var i = document.createElement('iframe'); + i.srcdoc = `${markup}sekrit`; + return i; + } + + // The following resources should not be blocked, as their URLs do not contain both a `\n` and `<` + // character in the body of the URL. + var should_load = [ + // Brace alone doesn't block: + `<img id="dangling" src="/images/green-1x1.png?img=${rawBrace}b">`, + + // Newline alone doesn't block: + `<img id="dangling" src="/images/green-1x1.png?img=${rawNewline}b">`, + + // Entity-escaped characters don't trigger blocking: + `<img id="dangling" src="/images/green-1x1.png?img=${escapedNewline}b">`, + `<img id="dangling" src="/images/green-1x1.png?img=${escapedBrace}b">`, + `<img id="dangling" src="/images/green-1x1.png?img=${escapedNewline}b${escapedBrace}c">`, + + // Leading and trailing whitespace is stripped: + ` + <img id="dangling" src=" + /images/green-1x1.png?img= + "> + `, + ` + <img id="dangling" src=" + /images/green-1x1.png?img=${escapedBrace} + "> + `, + ` + <img id="dangling" src=" + /images/green-1x1.png?img=${escapedNewline} + "> + `, + ]; + + should_load.forEach(markup => { + async_test(t => { + var i = createFrame(`${markup} <element attr="" another=''>`); + assert_img_loaded(t, i); + }, readableURL(markup)); + }); + + // The following resources should be blocked, as their URLs contain both `\n` and `<` characters: + var should_block = [ + `<img id="dangling" src="/images/green-1x1.png?img=${rawNewline}${rawBrace}b">`, + `<img id="dangling" src="/images/green-1x1.png?img=${rawBrace}${rawNewline}b">`, + ` + <img id="dangling" src="/images/green-1x1.png?img= + ${rawBrace} + ${rawNewline}b + "> + `, + ]; + + should_block.forEach(markup => { + async_test(t => { + var i = createFrame(`${markup}`); + assert_img_not_loaded(t, i); + }, readableURL(markup)); + }); +</script> diff --git a/testing/web-platform/tests/fetch/security/embedded-credentials.tentative.sub.html b/testing/web-platform/tests/fetch/security/embedded-credentials.tentative.sub.html new file mode 100644 index 0000000000..ca5ee1c87b --- /dev/null +++ b/testing/web-platform/tests/fetch/security/embedded-credentials.tentative.sub.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + async_test(t => { + var i = document.createElement('img'); + i.onerror = t.step_func_done(); + i.onload = t.unreached_func("'onload' should not fire."); + i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/red.png"; + }, "Embedded credentials are treated as network errors."); + + async_test(t => { + var i = document.createElement('iframe'); + i.src = "./support/embedded-credential-window.sub.html"; + i.onload = t.step_func(_ => { + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + assert_equals(e.data, "Error", "The image should not load."); + i.remove(); + }); + i.contentWindow.postMessage("Hi!", "*", [c.port2]); + }); + document.body.appendChild(i); + }, "Embedded credentials are treated as network errors in frames."); + + async_test(t => { + var w = window.open("./support/embedded-credential-window.sub.html"); + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Error", "The image should not load."); + }); + w.postMessage("absolute", "*", [c.port2]); + })); + }, "Embedded credentials are treated as network errors in new windows."); + + async_test(t => { + var w = window.open(); + w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html"; + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Load", "The image should load."); + }); + w.postMessage("relative", "*", [c.port2]); + })); + }, "Embedded credentials matching the top-level are not treated as network errors for relative URLs."); + + async_test(t => { + var w = window.open(); + w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html"; + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Load", "The image should load."); + }); + w.postMessage("same-origin-matching", "*", [c.port2]); + })); + }, "Embedded credentials matching the top-level are not treated as network errors for same-origin URLs."); + + async_test(t => { + var w = window.open(); + w.location.href = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/fetch/security/support/embedded-credential-window.sub.html"; + window.addEventListener("message", t.step_func(message => { + if (message.source != w) + return; + + var c = new MessageChannel(); + c.port1.onmessage = t.step_func_done(e => { + w.close(); + assert_equals(e.data, "Error", "The image should load."); + }); + w.postMessage("cross-origin-matching", "*", [c.port2]); + })); + }, "Embedded credentials matching the top-level are treated as network errors for cross-origin URLs."); +</script> diff --git a/testing/web-platform/tests/fetch/security/redirect-to-url-with-credentials.https.html b/testing/web-platform/tests/fetch/security/redirect-to-url-with-credentials.https.html new file mode 100644 index 0000000000..b06464805c --- /dev/null +++ b/testing/web-platform/tests/fetch/security/redirect-to-url-with-credentials.https.html @@ -0,0 +1,68 @@ +<html> +<header> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/common/get-host-info.sub.js"></script> +</header> +<body> +<script> +var host = get_host_info(); + +var sameOriginImageURL = "/common/redirect.py?location=" + host.HTTPS_ORIGIN_WITH_CREDS + "/service-workers/service-worker/resources/fetch-access-control.py?ACAOrigin= " + host.HTTPS_ORIGIN + "%26PNGIMAGE%26ACACredentials=true"; +var imageURL = "/common/redirect.py?location=" + host.HTTPS_REMOTE_ORIGIN_WITH_CREDS + "/service-workers/service-worker/resources/fetch-access-control.py?ACAOrigin= " + host.HTTPS_ORIGIN + "%26PNGIMAGE%26ACACredentials=true"; +var frameURL = "/common/redirect.py?location=" + host.HTTPS_REMOTE_ORIGIN_WITH_CREDS + "/common/blank.html"; + +promise_test((test) => { + return fetch(imageURL, {mode: "no-cors"}); +}, "No CORS fetch after a redirect with an URL containing credentials"); + +promise_test((test) => { + return promise_rejects_js(test, TypeError, fetch(imageURL, {mode: "cors"})); +}, "CORS fetch after a redirect with a cross origin URL containing credentials"); + +promise_test((test) => { + return fetch(sameOriginImageURL, {mode: "cors"}); +}, "CORS fetch after a redirect with a same origin URL containing credentials"); + +promise_test((test) => { + return new Promise((resolve, reject) => { + var image = new Image(); + image.onload = resolve; + image.onerror = (e) => reject(e); + image.src = imageURL; + }); +}, "Image loading after a redirect with an URL containing credentials"); + +promise_test((test) => { + return new Promise((resolve, reject) => { + var image = new Image(); + image.crossOrigin = "use-credentials"; + image.onerror = resolve; + image.onload = () => reject("Image should not load"); + image.src = imageURL; + }); +}, "CORS Image loading after a redirect with a cross origin URL containing credentials"); + +promise_test((test) => { + return new Promise((resolve, reject) => { + var image = new Image(); + image.crossOrigin = "use-credentials"; + image.onload = resolve; + image.onerror = (e) => reject(e); + image.src = sameOriginImageURL; + }); +}, "CORS Image loading after a redirect with a same origin URL containing credentials"); + +promise_test(async (test) => { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + await new Promise((resolve, reject) => { + iframe.src = frameURL; + iframe.onload = resolve; + iframe.onerror = (e) => reject(e); + }); + document.body.removeChild(iframe); +}, "Frame loading after a redirect with an URL containing credentials"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/fetch/security/support/embedded-credential-window.sub.html b/testing/web-platform/tests/fetch/security/support/embedded-credential-window.sub.html new file mode 100644 index 0000000000..20d307e918 --- /dev/null +++ b/testing/web-platform/tests/fetch/security/support/embedded-credential-window.sub.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script> + window.addEventListener("message", e => { + var i = document.createElement('img'); + i.onload = () => { e.ports[0].postMessage("Load"); } + i.onerror = () => { e.ports[0].postMessage("Error"); } + if (e.data == "relative") { + i.src = "/images/green.png"; + } else if (e.data == "same-origin-matching") { + i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/green.png"; + } else if (e.data == "cross-origin-matching") { + i.src = "http://user:pass@{{domains[élève]}}:{{ports[http][0]}}/images/red.png"; + } else { + i.src = "http://user:pass@{{domains[www]}}:{{ports[http][0]}}/images/red.png"; + } + }); + + (window.opener || window.parent).postMessage("Hi!", "*"); +</script> |