Revision f8dedf60
Added by koszko about 1 year ago
common/entities.js | ||
---|---|---|
116 | 116 |
} |
117 | 117 |
#EXPORT get_used_files AS get_files |
118 | 118 |
|
119 |
/* |
|
120 |
* Regex to parse URIs like: |
|
121 |
* https://hydrilla.koszko.org/schemas/api_mapping_description-2.schema.json |
|
122 |
*/ |
|
123 |
const name_base_re = "(?<name_base>[^/]*)"; |
|
124 |
const major_number_re = "(?<major>[1-9][0-9]*)"; |
|
125 |
const minor_number_re = "(?:[1-9][0-9]*|0)"; |
|
126 |
const numbers_rest_re = `(?:\\.${minor_number_re})*`; |
|
127 |
const version_re = `(?<ver>${major_number_re}${numbers_rest_re})`; |
|
128 |
const schema_name_re = `${name_base_re}-${version_re}\\.schema\\.json`; |
|
129 |
|
|
130 |
const haketilo_schema_name_regex = new RegExp(schema_name_re); |
|
131 |
#EXPORT haketilo_schema_name_regex |
|
132 |
|
|
133 |
/* Extract the number that indicates entity's compatibility mode. */ |
|
134 |
function get_schema_major_version(instance) { |
|
135 |
const match = haketilo_schema_name_regex.exec(instance.$schema); |
|
136 |
|
|
137 |
return parseInt(match.groups.major); |
|
138 |
} |
|
139 |
#EXPORT get_schema_major_version |
|
140 |
|
|
119 | 141 |
#IF NEVER |
120 | 142 |
|
121 | 143 |
/* |
common/jsonschema.js | ||
---|---|---|
57 | 57 |
|
58 | 58 |
#FROM common/jsonschema/scan.js IMPORT SchemaScanResult, scan |
59 | 59 |
|
60 |
#FROM common/entities.js IMPORT haketilo_schema_name_regex |
|
61 |
|
|
60 | 62 |
#EXPORT scan |
61 | 63 |
#EXPORT SchemaScanResult |
62 | 64 |
|
... | ... | |
86 | 88 |
#INCLUDE schemas/2.x/common_definitions-2.schema.json |
87 | 89 |
].reduce((ac, s) => Object.assign(ac, {[s.$id]: s}), {}); |
88 | 90 |
|
89 |
const name_base_re = "(?<name_base>[^/]*)"; |
|
90 |
const major_number_re = "(?<major>[1-9][0-9]*)"; |
|
91 |
const minor_number_re = "(?:[1-9][0-9]*|0)"; |
|
92 |
const numbers_rest_re = `(?:\\.${minor_number_re})*`; |
|
93 |
const version_re = `(?<ver>${major_number_re}${numbers_rest_re})`; |
|
94 |
const schema_name_re = `${name_base_re}-${version_re}\\.schema\\.json`; |
|
95 |
|
|
96 |
const haketilo_schema_name_regex = new RegExp(schema_name_re); |
|
97 |
|
|
98 | 91 |
for (const [$id, schema] of [...Object.entries(haketilo_schemas)]) { |
99 | 92 |
const match = haketilo_schema_name_regex.exec($id); |
100 | 93 |
const schema_name = |
... | ... | |
103 | 96 |
} |
104 | 97 |
|
105 | 98 |
#EXPORT haketilo_schemas |
106 |
#EXPORT haketilo_schema_name_regex |
|
107 | 99 |
|
108 | 100 |
const haketilo_validator = new Validator(); |
109 | 101 |
Object.values(haketilo_schemas) |
common/policy.js | ||
---|---|---|
49 | 49 |
* CSP rule that either blocks all scripts or only allows scripts with specified |
50 | 50 |
* nonce attached. |
51 | 51 |
*/ |
52 |
function make_csp(nonce) |
|
53 |
{ |
|
54 |
const rule = nonce ? `nonce-${nonce}` : "none"; |
|
52 |
function make_csp(nonce) { |
|
53 |
const rule = nonce ? `'nonce-${nonce}'` : "'none'"; |
|
55 | 54 |
const csp_list = [ |
56 |
["prefetch-src", "none"],
|
|
57 |
["script-src-attr", "none"],
|
|
58 |
["script-src", rule], |
|
55 |
["prefetch-src", "'none'"],
|
|
56 |
["script-src-attr", "'none'"],
|
|
57 |
["script-src", rule, "'unsafe-eval'"],
|
|
59 | 58 |
["script-src-elem", rule] |
60 | 59 |
]; |
61 |
return csp_list.map(([a, b]) => `${a} '${b}';`).join(" ");
|
|
60 |
return csp_list.map(words => `${words.join(" ")};`).join(" ");
|
|
62 | 61 |
} |
63 | 62 |
|
64 | 63 |
function decide_policy(patterns_tree, url, default_allow, secret) |
... | ... | |
113 | 112 |
#EXPORT decide_policy |
114 | 113 |
|
115 | 114 |
#EXPORT () => ({allow: false, csp: make_csp()}) AS fallback_policy |
115 |
|
|
116 |
#IF NEVER |
|
117 |
|
|
118 |
/* |
|
119 |
* Note: the functions below were overeagerly written and are not used now but |
|
120 |
* might prove useful to once we add more functionalities and are hence kept... |
|
121 |
*/ |
|
122 |
|
|
123 |
function relaxed_csp_eval(csp) { |
|
124 |
const new_csp_list = []; |
|
125 |
|
|
126 |
for (const directive of csp.split(";")) { |
|
127 |
const directive_words = directive.trim().split(" "); |
|
128 |
if (directive_words[0] === "script-src") |
|
129 |
directive_words.push("'unsafe-eval'"); |
|
130 |
|
|
131 |
new_csp_list.push(directive_words); |
|
132 |
} |
|
133 |
|
|
134 |
new_policy.csp = new_csp_list.map(d => `${d.join(" ")}';`).join(" "); |
|
135 |
} |
|
136 |
|
|
137 |
function relax_policy_eval(policy) { |
|
138 |
const new_policy = Object.assign({}, policy); |
|
139 |
|
|
140 |
return Object.assign(new_policy, {csp: relaxed_csp_eval(policy.csp)}); |
|
141 |
} |
|
142 |
#EXPORT relax_policy_eval |
|
143 |
|
|
144 |
#ENDIF |
html/install.js | ||
---|---|---|
49 | 49 |
#FROM html/DOM_helpers.js IMPORT clone_template, Showable |
50 | 50 |
#FROM common/entities.js IMPORT item_id_string, version_string, get_files |
51 | 51 |
#FROM common/misc.js IMPORT sha256_async AS compute_sha256 |
52 |
#FROM common/jsonschema.js IMPORT haketilo_validator, haketilo_schemas, \
|
|
53 |
haketilo_schema_name_regex
|
|
52 |
#FROM common/jsonschema.js IMPORT haketilo_validator, haketilo_schemas |
|
53 |
#FROM common/entities.js IMPORT haketilo_schema_name_regex
|
|
54 | 54 |
|
55 | 55 |
#FROM html/repo_query_cacher_client.js IMPORT indirect_fetch |
56 | 56 |
|
test/haketilo_test/unit/test_policy_deciding.py | ||
---|---|---|
23 | 23 |
|
24 | 24 |
from ..script_loader import load_script |
25 | 25 |
|
26 |
csp_re = re.compile(r'^\S+\s+\S+;(?:\s+\S+\s+\S+;)*$') |
|
27 |
rule_re = re.compile(r'^\s*(?P<src_kind>\S+)\s+(?P<allowed_origins>\S+)$') |
|
26 |
csp_re = re.compile(r''' |
|
27 |
^ |
|
28 |
\S+(?:\s+\S+)+; # first directive |
|
29 |
(?: |
|
30 |
\s+\S+(?:\s+\S+)+; # subsequent directive |
|
31 |
)* |
|
32 |
$ |
|
33 |
''', |
|
34 |
re.VERBOSE) |
|
35 |
|
|
36 |
rule_re = re.compile(r''' |
|
37 |
^ |
|
38 |
\s* |
|
39 |
(?P<src_kind>\S+) |
|
40 |
\s+ |
|
41 |
(?P<allowed_origins> |
|
42 |
\S+(?:\s+\S+)* |
|
43 |
) |
|
44 |
$ |
|
45 |
''', re.VERBOSE) |
|
46 |
|
|
28 | 47 |
def parse_csp(csp): |
29 |
''' |
|
30 |
Parsing of CSP string into a dict. A simplified format of CSP is assumed. |
|
31 |
''' |
|
48 |
'''Parsing of CSP string into a dict.''' |
|
32 | 49 |
assert csp_re.match(csp) |
33 | 50 |
|
34 | 51 |
result = {} |
35 | 52 |
|
36 | 53 |
for rule in csp.split(';')[:-1]: |
37 | 54 |
match = rule_re.match(rule) |
38 |
result[match.group('src_kind')] = match.group('allowed_origins') |
|
55 |
result[match.group('src_kind')] = match.group('allowed_origins').split()
|
|
39 | 56 |
|
40 | 57 |
return result |
41 | 58 |
|
... | ... | |
78 | 95 |
for prop in ('mapping', 'payload', 'nonce', 'error'): |
79 | 96 |
assert prop not in policy |
80 | 97 |
assert parse_csp(policy['csp']) == { |
81 |
'prefetch-src': "'none'",
|
|
82 |
'script-src-attr': "'none'",
|
|
83 |
'script-src': "'none'",
|
|
84 |
'script-src-elem': "'none'"
|
|
98 |
'prefetch-src': ["'none'"],
|
|
99 |
'script-src-attr': ["'none'"],
|
|
100 |
'script-src': ["'none'", "'unsafe-eval'"],
|
|
101 |
'script-src-elem': ["'none'"]
|
|
85 | 102 |
} |
86 | 103 |
|
87 | 104 |
policy = execute_in_page( |
... | ... | |
95 | 112 |
for prop in ('payload', 'nonce', 'error'): |
96 | 113 |
assert prop not in policy |
97 | 114 |
assert parse_csp(policy['csp']) == { |
98 |
'prefetch-src': "'none'",
|
|
99 |
'script-src-attr': "'none'",
|
|
100 |
'script-src': "'none'",
|
|
101 |
'script-src-elem': "'none'"
|
|
115 |
'prefetch-src': ["'none'"],
|
|
116 |
'script-src-attr': ["'none'"],
|
|
117 |
'script-src': ["'none'", "'unsafe-eval'"],
|
|
118 |
'script-src-elem': ["'none'"]
|
|
102 | 119 |
} |
103 | 120 |
|
104 | 121 |
policy = execute_in_page( |
... | ... | |
114 | 131 |
assert policy['nonce'] == \ |
115 | 132 |
sha256('m1:res1:http://kno.wn/:abcd'.encode()).digest().hex() |
116 | 133 |
assert parse_csp(policy['csp']) == { |
117 |
'prefetch-src': f"'none'",
|
|
118 |
'script-src-attr': f"'none'",
|
|
119 |
'script-src': f"'nonce-{policy['nonce']}'",
|
|
120 |
'script-src-elem': f"'nonce-{policy['nonce']}'"
|
|
134 |
'prefetch-src': ["'none'"],
|
|
135 |
'script-src-attr': ["'none'"],
|
|
136 |
'script-src': [f"'nonce-{policy['nonce']}'", "'unsafe-eval'"],
|
|
137 |
'script-src-elem': [f"'nonce-{policy['nonce']}'"]
|
|
121 | 138 |
} |
122 | 139 |
|
123 | 140 |
policy = execute_in_page( |
... | ... | |
128 | 145 |
for prop in ('mapping', 'payload', 'nonce'): |
129 | 146 |
assert prop not in policy |
130 | 147 |
assert parse_csp(policy['csp']) == { |
131 |
'prefetch-src': "'none'",
|
|
132 |
'script-src-attr': "'none'",
|
|
133 |
'script-src': "'none'",
|
|
134 |
'script-src-elem': "'none'"
|
|
148 |
'prefetch-src': ["'none'"],
|
|
149 |
'script-src-attr': ["'none'"],
|
|
150 |
'script-src': ["'none'", "'unsafe-eval'"],
|
|
151 |
'script-src-elem': ["'none'"]
|
|
135 | 152 |
} |
test/haketilo_test/unit/test_policy_enforcing.py | ||
---|---|---|
31 | 31 |
allow_policy = {'allow': True} |
32 | 32 |
block_policy = { |
33 | 33 |
'allow': False, |
34 |
'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'none'; script-src-elem 'none'; frame-src http://* https://*;" |
|
34 |
'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'none' 'unsafe-eval'; script-src-elem 'none'; frame-src http://* https://*;"
|
|
35 | 35 |
} |
36 | 36 |
payload_policy = { |
37 | 37 |
'mapping': 'somemapping', |
38 | 38 |
'payload': {'identifier': 'someresource'}, |
39 |
'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-{nonce}'; script-src-elem 'nonce-{nonce}';" |
|
39 |
'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-{nonce}' 'unsafe-eval'; script-src-elem 'nonce-{nonce}';"
|
|
40 | 40 |
} |
41 | 41 |
|
42 | 42 |
def content_script(): |
test/haketilo_test/unit/test_webrequest.py | ||
---|---|---|
85 | 85 |
payload_csp_header = { |
86 | 86 |
'name': f'Content-Security-Policy', |
87 | 87 |
'value': ("prefetch-src 'none'; script-src-attr 'none'; " |
88 |
f"script-src '{nonce}'; script-src-elem '{nonce}';") |
|
88 |
f"script-src '{nonce}' 'unsafe-eval'; script-src-elem '{nonce}';")
|
|
89 | 89 |
} |
90 | 90 |
|
91 | 91 |
sample_payload_headers = [ |
... | ... | |
107 | 107 |
sample_blocked_headers.append({ |
108 | 108 |
'name': f'Content-Security-Policy', |
109 | 109 |
'value': ("prefetch-src 'none'; script-src-attr 'none'; " |
110 |
f"script-src 'none'; script-src-elem 'none';")
|
|
110 |
"script-src 'none' 'unsafe-eval'; script-src-elem 'none';")
|
|
111 | 111 |
}) |
112 | 112 |
|
113 | 113 |
@pytest.mark.get_page('https://gotmyowndoma.in') |
test/haketilo_test/unit/utils.py | ||
---|---|---|
228 | 228 |
return driver.execute_script( |
229 | 229 |
''' |
230 | 230 |
document.haketilo_scripts_allowed = false; |
231 |
document.haketilo_eval_allowed = false; |
|
231 | 232 |
const html_ns = "http://www.w3.org/1999/xhtml"; |
232 | 233 |
const script = document.createElementNS(html_ns, "script"); |
233 |
script.innerHTML = "document.haketilo_scripts_allowed = true;"; |
|
234 |
script.innerHTML = ` |
|
235 |
document.haketilo_scripts_allowed = true; |
|
236 |
eval('document.haketilo_eval_allowed = true;'); |
|
237 |
`; |
|
234 | 238 |
if (arguments[0]) |
235 | 239 |
script.setAttribute("nonce", arguments[0]); |
236 | 240 |
(document.head || document.documentElement).append(script); |
241 |
|
|
242 |
if (document.haketilo_scripts_allowed != |
|
243 |
document.haketilo_eval_allowed) |
|
244 |
throw "scripts allowed but eval blocked"; |
|
245 |
|
|
237 | 246 |
return document.haketilo_scripts_allowed; |
238 | 247 |
''', |
239 | 248 |
nonce) |
test/haketilo_test/world_wide_library.py | ||
---|---|---|
152 | 152 |
'id_suffix': 'a-w-required-mapping-v1', |
153 | 153 |
'files_count': 1, |
154 | 154 |
'dependencies': [], |
155 |
'required_mappings': [{'identifier': 'mapping-a'}] |
|
155 |
'required_mappings': [{'identifier': 'mapping-a'}], |
|
156 |
'include_in_query': False |
|
156 | 157 |
}) |
157 | 158 |
|
158 | 159 |
sample_resource_templates.append({ |
... | ... | |
160 | 161 |
'files_count': 1, |
161 | 162 |
'dependencies': [], |
162 | 163 |
'required_mappings': [{'identifier': 'mapping-a'}], |
163 |
'schema_ver': '2' |
|
164 |
'schema_ver': '2', |
|
165 |
'include_in_query': False |
|
164 | 166 |
}) |
165 | 167 |
|
166 | 168 |
sample_resources_catalog = {} |
... | ... | |
193 | 195 |
|
194 | 196 |
sufs = [srt["id_suffix"], *[l for l in srt["id_suffix"] if l.isalpha()]] |
195 | 197 |
patterns = [f'https://example_{suf}.com/*' for suf in set(sufs)] |
196 |
payloads = {}
|
|
198 |
mapping['payloads'] = {}
|
|
197 | 199 |
|
198 | 200 |
for pat in patterns: |
199 |
payloads[pat] = {'identifier': resource['identifier']}
|
|
201 |
mapping['payloads'][pat] = {'identifier': resource['identifier']}
|
|
200 | 202 |
|
201 |
queryable_url = pat.replace('*', 'something') |
|
202 |
if queryable_url not in sample_queries: |
|
203 |
sample_queries[queryable_url] = [] |
|
203 |
if not srt.get('include_in_query', True): |
|
204 |
continue |
|
204 | 205 |
|
205 |
sample_queries[queryable_url].append({
|
|
206 |
sample_queries.setdefault(pat.replace('*', 'something'), []).append({
|
|
206 | 207 |
'identifier': mapping['identifier'], |
207 | 208 |
'long_name': mapping['long_name'], |
208 | 209 |
'version': mapping_versions[1] |
209 | 210 |
}) |
210 | 211 |
|
211 |
mapping['payloads'] = payloads |
|
212 |
|
|
213 | 212 |
for item in resource, mapping: |
214 | 213 |
if 'required_mappings' in srt: |
215 | 214 |
item['required_mappings'] = srt['required_mappings'] |
Also available in: Unified diff
allow eval() in injected scripts