Skip to content

Commit

Permalink
Testing "attack 4"
Browse files Browse the repository at this point in the history
  • Loading branch information
jakearchibald committed Apr 10, 2018
1 parent 9e34bb1 commit 0e4a090
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 101 deletions.
19 changes: 0 additions & 19 deletions fetch/privileged-headers/resources/partial.py

This file was deleted.

54 changes: 0 additions & 54 deletions fetch/privileged-headers/sw.https.window.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
// Helpers that return headers objects with a particular guard
function headersGuardNone(fill) {
if (fill) return new Headers(fill);
return new Headers();
}

function headersGuardResponse(fill) {
if (fill) return new Response('', { headers: fill }).headers;
return new Response('').headers;
const opts = {};
if (fill) opts.headers = fill;
return new Response('', opts).headers;
}

function headersGuardRequest(fill) {
if (fill) return new Request('./', { headers: fill }).headers;
return new Request('./').headers;
const opts = {};
if (fill) opts.headers = fill;
return new Request('./', opts).headers;
}

function headersGuardRequestNoCors(fill) {
if (fill) return new Request('./', { headers: fill, mode: 'no-cors' }).headers;
return new Request('./', { mode: 'no-cors' }).headers;
const opts = { mode: 'no-cors' };
if (fill) opts.headers = fill;
return new Request('./', opts).headers;
}

test(() => {
// Setting range should work for these guards
for (const createHeaders of [headersGuardNone, headersGuardResponse, headersGuardRequest]) {
// There are three ways to set headers.
// Filling, appending, and setting. Test each:
let headers = createHeaders({ Range: 'foo' });
assert_equals(headers.get('Range'), 'foo');

Expand All @@ -45,3 +51,4 @@ test(() => {
headers.set('Range', 'foo');
assert_false(headers.has('Range'));
}, `Privileged header is allowed unless guard is request-no-cors`);

7 changes: 7 additions & 0 deletions fetch/range/partial-script.window.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// META: script=resources/utils.js

// It's weird that browsers do this, but it should continue to work.
promise_test(async t => {
await loadScript('resources/partial-script.py?pretend-offset=90000');
assert_true(self.scriptExecuted);
}, `Script executed from partial response`);
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,17 @@ def main(request, response):
range_received_key = request.GET.first('range-received-key', '')

if range_received_key and range_header:
# This is later collected using stash-take.py
request.stash.put(range_received_key, 'range-header-received', '/fetch/privileged-headers/')

# Audio details
sample_rate = 8000
bit_depth = 8
channels = 1
duration = 60 * 5

total_length = (sample_rate * bit_depth * channels * duration) / 8
bytes_to_send = total_length
bytes_remaining_to_send = total_length
initial_write = ''

if range_header:
Expand All @@ -72,37 +74,38 @@ def main(request, response):
end = int(end) if end else 0

if end:
bytes_to_send = (end + 1) - start
bytes_remaining_to_send = (end + 1) - start
else:
bytes_to_send = total_length - start
bytes_remaining_to_send = total_length - start

wav_header = create_wav_header(sample_rate, bit_depth, channels, duration)

if start < len(wav_header):
initial_write = wav_header[start:]

if bytes_to_send < len(initial_write):
initial_write = initial_write[0:bytes_to_send]
if bytes_remaining_to_send < len(initial_write):
initial_write = initial_write[0:bytes_remaining_to_send]

content_range = "bytes {}-{}/{}".format(start, end or total_length - 1, total_length)

response.headers.set("Content-Range", content_range)
else:
initial_write = create_wav_header(sample_rate, bit_depth, channels, duration)

response.headers.set("Content-Length", bytes_to_send)
response.headers.set("Content-Length", bytes_remaining_to_send)

response.write_status_headers()
response.writer.write(initial_write)

bytes_to_send -= len(initial_write)
bytes_remaining_to_send -= len(initial_write)

while bytes_to_send > 0:
while bytes_remaining_to_send > 0:
if not response.writer.flush():
break

to_send = b'\x00' * min(bytes_to_send, sample_rate)
bytes_to_send -= len(to_send)
to_send = b'\x00' * min(bytes_remaining_to_send, sample_rate)
bytes_remaining_to_send -= len(to_send)

response.writer.write(to_send)
# Throttle the stream
time.sleep(0.5)
30 changes: 30 additions & 0 deletions fetch/range/resources/partial-script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
This generates a partial response containing valid JavaScript.
"""


def main(request, response):
require_range = request.GET.first('require-range', '')
pretend_offset = int(request.GET.first('pretend-offset', '0'))
range_header = request.headers.get('Range', '')

if require_range and not range_header:
response.set_error(412, "Range header required")
response.write()
return

response.headers.set("Content-Type", "text/plain")
response.headers.set("Accept-Ranges", "bytes")
response.headers.set("Cache-Control", "no-cache")
response.status = 206

to_send = 'self.scriptExecuted = true;'
length = len(to_send)

content_range = "bytes {}-{}/{}".format(
pretend_offset, pretend_offset + length - 1, pretend_offset + length)

response.headers.set("Content-Range", content_range)
response.headers.set("Content-Length", length)

response.content = to_send
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ function assert_range_request(request, expectedRangeHeader, name) {
assert_equals(request.headers.get('Range'), expectedRangeHeader, name);
}

async function broadcast(msg) {
for (const client of await clients.matchAll()) {
client.postMessage(msg);
}
}

addEventListener('fetch', event => {
/** @type Request */
const request = event.request;
Expand All @@ -19,26 +25,26 @@ addEventListener('fetch', event => {
case 'range-header-passthrough-test':
rangeHeaderPassthroughTest(event);
return;
case 'store-ranged-response':
storeRangedResponse(event);
return;
case 'use-stored-ranged-response':
useStoredRangeResponse(event);
return;
}
});

let gotRangeResponse = false;

/**
* @param {Request} request
*/
function rangeHeaderFilterTest(request) {
if (!request.headers.has('Range') || gotRangeResponse) return;
// Avoid running the test twice
gotRangeResponse = true;

const rangeValue = request.headers.get('Range');

test(() => {
assert_range_request(new Request(request), rangeValue, `Untampered`);
assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`);
assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`);
assert_range_request(new Request(request, { move: 'cors' }), rangeValue, `More permissive mode`);
assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`);
assert_range_request(request.clone(), rangeValue, `Clone`);
}, "Range headers correctly preserved");

Expand All @@ -54,7 +60,7 @@ function rangeHeaderFilterTest(request) {

headers = new Request(request).headers;
headers.delete('does-not-exist');
assert_equals(headers.get('Range'), rangeValue, `Preserved if no header removed`);
assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`);

headers = new Request(request).headers;
headers.append('foo', 'bar');
Expand Down Expand Up @@ -98,10 +104,6 @@ function rangeHeaderPassthroughTest(event) {
const url = new URL(request.url);
const key = url.searchParams.get('range-received-key');

if (!request.headers.has('Range') || gotRangeResponse) return;
// Avoid running the test twice
gotRangeResponse = true;

let waitUntilResolve;
const waitUntilPromise = new Promise(r => waitUntilResolve = r);
event.waitUntil(waitUntilPromise);
Expand All @@ -118,3 +120,25 @@ function rangeHeaderPassthroughTest(event) {

done();
}

let storedRangeResponseP;

function storeRangedResponse(event) {
/** @type Request */
const request = event.request;
const id = new URL(request.url).searchParams.get('id');

storedRangeResponseP = fetch(event.request);
broadcast({id});

// Just send back any response, it isn't important for the test.
event.respondWith(new Response(''));
}

function useStoredRangeResponse(event) {
event.respondWith(async function() {
const response = await storedRangeResponseP;
if (!response) throw Error("Expected stored range response");
return response.clone();
}());
}
File renamed without changes.
9 changes: 9 additions & 0 deletions fetch/range/resources/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function loadScript(url, { doc = document }={}) {
return new Promise((resolve, reject) => {
const script = doc.createElement('script');
script.onload = () => resolve();
script.onerror = () => reject(Error("Script load failed"));
script.src = url;
doc.body.appendChild(script);
})
}
Loading

0 comments on commit 0e4a090

Please sign in to comment.