Skip to content

Commit f027f57

Browse files
WIP: WPT tests for security features (CSP, mixed content, fetch metadata) applied to speculation rules resource fetch
Change-Id: I3e5de39191241ebee592c8e9ea85e38f462e1d7c
1 parent e0e0f5b commit f027f57

17 files changed

+897
-5
lines changed

common/security-features/resources/common.sub.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,37 @@ function requestViaWebSocket(url) {
826826
});
827827
}
828828

829+
/**
830+
* Requests speculation rules via the header in a new window.
831+
* @param {string} url The URL of the resource to request.
832+
* @return {Promise} The promise for success/error events.
833+
*/
834+
async function requestViaSpeculationRulesHeader(url) {
835+
console.error(url);
836+
let windowURL = `/common/blank.html?pipe=header(Speculation-Rules,"${encodeURIComponent(url)}")`;
837+
console.error(windowURL);
838+
let w = window.open(windowURL);
839+
try {
840+
// This is similar to t.step_wait, but t is not available here.
841+
let continuePolling = true;
842+
let status = 'blocked';
843+
const params = new URLSearchParams(new URL(url).search);
844+
const pollURL = `/common/security-features/subresource/xhr.py?action=peek&path=${encodeURIComponent(params.get('path'))}&key=${encodeURIComponent(params.get('key'))}`;
845+
step_timeout(() => continuePolling = false, 3000);
846+
do {
847+
// This sort of polling logic might be more generally useful to other
848+
// kinds of request that don't necessarily always receive a "done" event
849+
// of some kind. But at the moment that would be a more radical refactor.
850+
await new Promise(resolve => step_timeout(resolve, 200));
851+
let json = await fetch(pollURL).then(r => r.json());
852+
status = json.status;
853+
} while (status !== 'allowed' && continuePolling);
854+
return status;
855+
} finally {
856+
w.close();
857+
}
858+
}
859+
829860
/**
830861
@typedef SubresourceType
831862
@type {string}
@@ -942,6 +973,11 @@ const subresourceMap = {
942973
path: "/stash_responder",
943974
invoker: requestViaWebSocket,
944975
},
976+
977+
"speculationrules": {
978+
path: "/common/security-features/subresource/speculationrules.py",
979+
invoker: requestViaSpeculationRulesHeader,
980+
},
945981
};
946982
for (const workletType of ['animation', 'audio', 'layout', 'paint']) {
947983
subresourceMap[`worklet-${workletType}`] = {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import importlib
2+
subresource = importlib.import_module("common.security-features.subresource.subresource")
3+
4+
def generate_payload(server_data):
5+
return subresource.get_template(u"speculationrules.json.template") % server_data
6+
7+
def main(request, response):
8+
import sys
9+
print('got speculation rules request', file=sys.stderr)
10+
subresource.respond(request,
11+
response,
12+
payload_generator = generate_payload,
13+
content_type = b"application/speculationrules+json")

common/security-features/subresource/subresource.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ def preprocess_stash_action(request, response):
139139
else:
140140
status = u"blocked"
141141
response_data = json.dumps({u"status": status, u"result": value})
142+
elif action == b"peek":
143+
value = stash.take(key=key, path=path)
144+
stash.put(key=key, value=value, path=path)
145+
if value is None:
146+
status = u"allowed"
147+
else:
148+
status = u"blocked"
149+
response_data = json.dumps({u"status": status, u"result": value})
142150
else:
143151
return False
144152

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

common/security-features/tools/generate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def generate_test_source_files(spec_directory, test_helper_filenames,
321321
# Choose a debug/release template depending on the target.
322322
html_template = "test.%s.html.template" % target
323323

324-
artifact_order = test_expansion_schema.keys()
324+
artifact_order = list(test_expansion_schema.keys())
325325
artifact_order.remove('expansion')
326326

327327
excluded_selection_pattern = ''

common/security-features/tools/spec.src.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"sharedworker-import",
112112
"sharedworker-import-data",
113113
"sharedworker-module",
114+
"speculationrules",
114115
"video-tag",
115116
"worker-classic",
116117
"worker-import",
@@ -156,6 +157,31 @@
156157
],
157158
"origin": "*",
158159
"expectation": "*"
160+
},
161+
{
162+
// Only test speculation rules in the top document.
163+
"expansion": "*",
164+
"source_scheme": "*",
165+
"source_context_list": [
166+
"srcdoc-inherit",
167+
"srcdoc",
168+
"iframe",
169+
"iframe-blank-inherit",
170+
"worker-classic",
171+
"worker-classic-data",
172+
"worker-module",
173+
"worker-module-data",
174+
"sharedworker-classic",
175+
"sharedworker-classic-data",
176+
"sharedworker-module",
177+
"sharedworker-module-data"
178+
],
179+
"delivery_type": "*",
180+
"delivery_value": "*",
181+
"redirection": "*",
182+
"subresource": "speculationrules",
183+
"origin": "*",
184+
"expectation": "*"
159185
}
160186
],
161187
"source_context_schema": {
@@ -513,6 +539,7 @@
513539
"sharedworker-import",
514540
"sharedworker-import-data",
515541
"sharedworker-module",
542+
"speculationrules",
516543
"video-tag",
517544
"websocket",
518545
"worker-classic",

common/security-features/tools/spec_validator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
def assert_non_empty_string(obj, field):
77
assert field in obj, 'Missing field "%s"' % field
8-
assert isinstance(obj[field], basestring), \
8+
assert isinstance(obj[field], str), \
99
'Field "%s" must be a string' % field
1010
assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field
1111

@@ -34,7 +34,7 @@ def assert_value_from(obj, field, items):
3434

3535

3636
def assert_atom_or_list_items_from(obj, field, items):
37-
if isinstance(obj[field], basestring) or isinstance(
37+
if isinstance(obj[field], str) or isinstance(
3838
obj[field], int) or obj[field] is None:
3939
assert_value_from(obj, field, items)
4040
return
@@ -126,6 +126,8 @@ def validate(spec_json, details):
126126
"worklet-layout-import", "worklet-paint-import",
127127
"worklet-animation-import-data", "worklet-audio-import-data",
128128
"worklet-layout-import-data", "worklet-paint-import-data"
129+
] + [
130+
"speculationrules"
129131
]
130132

131133
# Validate each single spec.
@@ -236,7 +238,7 @@ def assert_valid_spec_json(spec_json):
236238
try:
237239
validate(spec_json, error_details)
238240
except AssertionError as err:
239-
print('ERROR:', err.message)
241+
print('ERROR:', err)
240242
print(json.dumps(error_details, indent=4))
241243
sys.exit(1)
242244

0 commit comments

Comments
 (0)