Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- This WPT test may be referenced by the following Test IDs:
- /cookies/path/match-percent-encoded.https.window.html - WPT Dashboard Interop Dashboard
// META: title=Cookie Path attribute matching against percent-encoded URL paths
'use strict';
// A cookie's path is compared against the request URL's path, whose segments are
// percent-encoded. So a byte written literally in a Path attribute does not match
// its percent-encoded form in a URL, and a non-ASCII byte, which makes the cookie
// fail to parse, is not reachable at any encoding of it.
//
// Observing this needs paths with no file behind them, so a service worker
// synthesizes a document at whichever path is probed. The document reads
// document.cookie, because a service worker cannot see a request's Cookie header.
//
const PROBE_DIR = '/cookies/path/resources/probe';
const NAME_PREFIX = 'match-percent-encoded-';
// Sets a Set-Cookie header, replacing "ZZ" in `cookie` with the raw bytes given
// as percent-escapes. wptserve percent-decodes the query into bytes and cookie.py
// passes those bytes to the header unchanged, which is how a byte that a URL path
// would percent-encode reaches the Path attribute as itself.
async function setCookieViaHTTP(cookie, rawEscape) {
let query = encodeURIComponent(JSON.stringify([cookie]));
if (rawEscape !== undefined) {
query = query.replace('ZZ', rawEscape);
}
const response = await fetch(`/cookies/resources/cookie.py?set=${query}`);
assert_true(response.ok, 'Setting the cookie via HTTP succeeded');
}
function cookieValue(cookieString, name) {
for (const pair of cookieString.split('; ')) {
const index = pair.indexOf('=');
if (index !== -1 && pair.substring(0, index) === name) {
return pair.substring(index + 1);
}
}
return null;
}
// Loads a synthesized document at `path` and returns what it reported.
function probeAtPath(path) {
return new Promise(resolve => {
const iframe = document.createElement('iframe');
iframe.style = 'display: none';
const onMessage = event => {
if (event.source !== iframe.contentWindow) {
return;
}
window.removeEventListener('message', onMessage);
iframe.remove();
resolve(event.data);
};
window.addEventListener('message', onMessage);
iframe.src = path;
document.body.appendChild(iframe);
});
}
let registration;
promise_setup(async () => {
registration = await navigator.serviceWorker.register(
'resources/probe/sw.js', {scope: `${PROBE_DIR}/`});
// navigator.serviceWorker.ready is not usable here: it waits for a worker
// controlling this document, and this document is outside the scope the probe
// paths need. Wait on the registration's own worker instead.
const worker =
registration.installing || registration.waiting || registration.active;
if (worker.state !== 'activated') {
await new Promise(resolve => {
worker.addEventListener('statechange', () => {
if (worker.state === 'activated') {
resolve();
}
});
});
}
});
// Sets the cookie under test, then expires it again through the same Path, which
// is what removes it.
function cookieTest(description, {suffix, pathSegment, raw}, body) {
const name = NAME_PREFIX + suffix;
promise_test(async t => {
t.add_cleanup(() => setCookieViaHTTP(
`${name}=; Max-Age=0; Path=${PROBE_DIR}/${pathSegment}`, raw));
await setCookieViaHTTP(`${name}=1; Path=${PROBE_DIR}/${pathSegment}`, raw);
await body(t, name);
}, description);
}
// Controls: the service worker synthesizes a document at the probed path and
// cookies for that path are visible in it, so the apparatus reports what it
// claims to.
cookieTest('CONTROL a cookie for the probed path is visible in it',
{suffix: 'control', pathSegment: 'zzx'}, async (t, name) => {
const result = await probeAtPath(`${PROBE_DIR}/zzx/probe.html`);
assert_equals(result.path, `${PROBE_DIR}/zzx/probe.html`,
'The document was synthesized at the probed path');
assert_equals(cookieValue(result.cookie, name), '1');
});
cookieTest('CONTROL a cookie for a prefix of the probed path is visible in it',
{suffix: 'prefix', pathSegment: ''}, async (t, name) => {
const result = await probeAtPath(`${PROBE_DIR}/zzx/probe.html`);
assert_equals(cookieValue(result.cookie, name), '1');
});
// Each case sets one cookie whose Path ends in `pathSegment`, with "ZZ" replaced
// by the raw bytes `raw` when given, then probes the URL path segment
// `probeSegment`.
const CASES = [
// A byte that is ASCII but that a URL path percent-encodes. The cookie is
// stored, but its path does not match the encoded segment.
{
name: 'a literal space does not match a percent-encoded space',
pathSegment: 'zzZZa',
raw: '%20',
probeSegment: 'zz%20a',
sent: false,
},
{
name: 'a percent-encoded space matches a percent-encoded space',
pathSegment: 'zz%20a',
probeSegment: 'zz%20a',
sent: true,
},
// A non-ASCII byte cannot be a URL path segment, so the cookie fails to parse
// and is reachable nowhere: neither at the percent-encoding of the byte, nor at
// the percent-encoding of its isomorphic decoding.
{
name: 'a non-ASCII byte is not reachable at the percent-encoding of that byte',
pathSegment: 'zzZZ',
raw: '%B8',
probeSegment: 'zz%B8',
sent: false,
},
{
name: 'a non-ASCII byte is not reachable at the percent-encoding of its isomorphic decoding',
pathSegment: 'zzZZ',
raw: '%B8',
probeSegment: 'zz%C2%B8',
sent: false,
},
{
name: 'a multi-byte non-ASCII sequence is not reachable at the percent-encoding of those bytes',
pathSegment: 'zzZZ',
raw: '%E4%B8%AD',
probeSegment: 'zz%E4%B8%AD',
sent: false,
},
{
name: 'a multi-byte non-ASCII sequence is not reachable at the percent-encoding of its isomorphic decoding',
pathSegment: 'zzZZ',
raw: '%E4%B8%AD',
probeSegment: 'zz%C3%A4%C2%B8%C2%AD',
sent: false,
},
{
name: 'an already percent-encoded non-ASCII byte matches, being ASCII itself',
pathSegment: 'zz%C2%B8',
probeSegment: 'zz%C2%B8',
sent: true,
},
];
for (const [index, testCase] of CASES.entries()) {
cookieTest(`A Path attribute where ${testCase.name}`,
{suffix: `case${index}`, pathSegment: testCase.pathSegment,
raw: testCase.raw},
async (t, name) => {
const result =
await probeAtPath(`${PROBE_DIR}/${testCase.probeSegment}/probe.html`);
assert_equals(cookieValue(result.cookie, name), testCase.sent ? '1' : null);
});
}
promise_test(async t => {
await registration.unregister();
}, 'cleanup: unregister the service worker');