-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstatic_policy.py
More file actions
110 lines (95 loc) · 4.43 KB
/
Copy pathstatic_policy.py
File metadata and controls
110 lines (95 loc) · 4.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""Independent static-file policy for the installed remote release endpoint.
An SSH request cannot change this policy or the server configuration it binds.
The operator must verify the handler guard on the actual web server before
enabling a production credential; filesystem fixtures do not prove HTTP behavior.
"""
import base64
import binascii
import re
import static_plan as plan
from site_artifact import ArtifactError, MANIFEST, require
MAX_CANDIDATE_BYTES = 8 * 1024 * 1024
MAX_STATIC_FILE_BYTES = 4 * 1024 * 1024
STATIC_GUARD = b'''# BEGIN OSS STATIC HANDLER GUARD v1
Options -ExecCGI -Includes -MultiViews
RemoveHandler .html .css .js .svg .webp .png .ico .json .xml .webmanifest .txt .sha256
RemoveInputFilter .html .css .js .svg .webp .png .ico .json .xml .webmanifest .txt .sha256
RemoveOutputFilter .html .css .js .svg .webp .png .ico .json .xml .webmanifest .txt .sha256
RemoveType .html .css .js .svg .webp .png .ico .json .xml .webmanifest .txt .sha256
AddType text/html .html
AddType text/css .css
AddType text/javascript .js
AddType image/svg+xml .svg
AddType image/webp .webp
AddType image/png .png
AddType image/x-icon .ico
AddType application/json .json
AddType application/xml .xml
AddType application/manifest+json .webmanifest
AddType text/plain .txt .sha256
<FilesMatch "\\.(?:html|css|js|svg|webp|png|ico|json|xml|webmanifest|txt|sha256)$">
SetHandler default-handler
AcceptPathInfo Off
</FilesMatch>
# END OSS STATIC HANDLER GUARD v1
'''
SLUG = r'[a-z0-9]+(?:-[a-z0-9]+)*'
ASSET = SLUG + r'(?:\.[a-f0-9]{8,64})?'
ROOT_FILES = {'index.html', '404.html', 'robots.txt', 'sitemap.xml', 'llms.txt',
'site.webmanifest', MANIFEST, '.htaccess',
'.well-known/agent-home.json', '.well-known/security.txt'}
def decode(value, limit):
require(type(value) is str and len(value) <= 4 * ((limit + 2) // 3), 'invalid_encoding')
try:
raw = base64.b64decode(value, validate=True)
except (ValueError, binascii.Error):
raise ArtifactError('invalid_encoding') from None
require(len(raw) <= limit and base64.b64encode(raw).decode('ascii') == value,
'invalid_encoding')
return raw
def allowed_path(name):
plan.path(name)
if name in ROOT_FILES:
return True
if name.split('/')[0] in {'api', 'cgi-bin'}:
return False
return any(re.fullmatch(pattern, name) is not None for pattern in (
SLUG + r'/index\.html',
r'assets/scripts/' + ASSET + r'\.js',
r'assets/styles/' + ASSET + r'\.css',
r'assets/(?:brand|projects|social)/' + ASSET + r'\.(?:svg|webp|png|ico)',
r'data/' + SLUG + r'(?:\.schema)?\.json',
))
def validate_policy(value):
plan.object_fields(value, {'schema', 'htaccess', 'installed_htaccess_sha256',
'ancestor_htaccess'}, 'invalid_policy')
require(type(value['schema']) is int and value['schema'] == 1, 'invalid_policy')
access = decode(value['htaccess'], 8192)
require(access and STATIC_GUARD not in access, 'invalid_policy')
plan.hex_value(value['installed_htaccess_sha256'], 64, 'invalid_policy')
require(type(value['ancestor_htaccess']) is dict
and len(value['ancestor_htaccess']) <= 32, 'invalid_policy')
for name, expected in value['ancestor_htaccess'].items():
require(type(name) is str and name.startswith('/'), 'invalid_policy')
if expected is not None:
plan.hex_value(expected, 64, 'invalid_policy')
return access
def validate_candidate(files, policy):
"""Additional server restrictions, independent of the runner's product tests."""
plan.payload(files)
access = validate_policy(policy)
require(files['.htaccess'] == access, 'server_configuration_changed')
require(sum(len(raw) for raw in files.values()) <= MAX_CANDIDATE_BYTES,
'candidate_limit')
for name, raw in files.items():
require(allowed_path(name), 'non_static_path')
require(len(raw) <= MAX_STATIC_FILE_BYTES, 'candidate_limit')
def validate_installed_access(raw, policy):
require(plan.digest(raw) == policy['installed_htaccess_sha256']
and raw.endswith(STATIC_GUARD) and raw.count(STATIC_GUARD) == 1,
'server_configuration_changed')
def validate_parents(names, entries):
"""No nested override may affect a path the credential can publish."""
for name in names:
for parent in plan.parents(name):
require(parent + '/.htaccess' not in entries, 'nested_server_configuration')