forked from pypi/warehouse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
csp.py
188 lines (156 loc) · 6.61 KB
/
csp.py
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import copy
import urllib.parse
from warehouse.config import Environment
SELF = "'self'"
NONE = "'none'"
def _serialize(policy):
return "; ".join(
[
" ".join([k] + [v2 for v2 in v if v2 is not None])
for k, v in sorted(policy.items())
]
)
def content_security_policy_tween_factory(handler, registry):
def content_security_policy_tween(request):
resp = handler(request)
try:
policy = request.find_service(name="csp")
except LookupError:
policy = collections.defaultdict(list)
# Replace CSP headers on /simple/ pages.
if request.path.startswith("/simple/"):
policy = collections.defaultdict(list)
policy["sandbox"] = ["allow-top-navigation"]
policy["default-src"] = [NONE]
# We don't want to apply our Content Security Policy to the debug
# toolbar, that's not part of our application and it doesn't work with
# our restrictive CSP.
policy = _serialize(policy).format(request=request)
if not request.path.startswith("/_debug_toolbar/") and policy:
resp.headers["Content-Security-Policy"] = policy
return resp
return content_security_policy_tween
class CSPPolicy(collections.defaultdict):
def __init__(self, policy=None):
super().__init__(list, policy or {})
def merge(self, policy):
for key, attrs in policy.items():
self[key].extend(attrs)
# The keyword 'none' must be the only source expression in the
# directive value, otherwise it is ignored. If there's more than
# one directive set, attempt to remove 'none' if it is present
if NONE in self[key] and len(self[key]) > 1:
self[key].remove(NONE)
def csp_factory(_, request):
try:
return CSPPolicy(copy.deepcopy(request.registry.settings["csp"]))
except KeyError:
return CSPPolicy({})
def _connect_src_settings(config) -> list:
settings = [
SELF,
"https://api.github.com/repos/",
"https://api.github.com/search/issues",
"https://*.google-analytics.com",
"https://*.analytics.google.com",
"https://*.googletagmanager.com",
"fastly-insights.com",
"*.fastly-insights.com",
"*.ethicalads.io",
"https://api.pwnedpasswords.com",
# Scoped deeply to prevent other scripts calling other CDN resources
"https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/sre/mathmaps/",
]
settings.extend(
[item for item in [config.registry.settings.get("statuspage.url")] if item]
)
if config.registry.settings.get("warehouse.env") == Environment.development:
livereload_url = config.registry.settings.get("livereload.url")
parsed_url = urllib.parse.urlparse(livereload_url)
# Incoming scheme could be http or https.
scheme_replacement = "wss" if parsed_url.scheme == "https" else "ws"
replaced = parsed_url._replace(scheme=scheme_replacement) # noqa
fixed = urllib.parse.urlunparse(replaced)
settings.extend(
[
f"{fixed}/livereload",
]
)
return settings
def _script_src_settings(config) -> list:
settings = [
SELF,
"https://*.googletagmanager.com",
"https://www.google-analytics.com", # Remove when disabling UA
"https://ssl.google-analytics.com", # Remove when disabling UA
"*.fastly-insights.com",
"*.ethicalads.io",
# Hash for v1.4.0 of ethicalads.min.js
"'sha256-U3hKDidudIaxBDEzwGJApJgPEf2mWk6cfMWghrAa6i0='",
"https://cdn.jsdelivr.net/npm/mathjax@3.2.2/",
# Hash for v3.2.2 of MathJax tex-svg.js
"'sha256-1CldwzdEg2k1wTmf7s5RWVd7NMXI/7nxxjJM2C4DqII='",
# Hash for MathJax inline config
# See warehouse/templates/packaging/detail.html
"'sha256-0POaN8stWYQxhzjKS+/eOfbbJ/u4YHO5ZagJvLpMypo='",
]
if config.registry.settings.get("warehouse.env") == Environment.development:
settings.extend(
[
f"{config.registry.settings['livereload.url']}/livereload.js",
]
)
return settings
def includeme(config):
config.register_service_factory(csp_factory, name="csp")
# Enable a Content Security Policy
config.add_settings(
{
"csp": {
"base-uri": [SELF],
"block-all-mixed-content": [],
"connect-src": _connect_src_settings(config),
"default-src": [NONE],
"font-src": [SELF, "fonts.gstatic.com"],
"form-action": [SELF, "https://checkout.stripe.com"],
"frame-ancestors": [NONE],
"frame-src": [NONE],
"img-src": [
SELF,
config.registry.settings["camo.url"],
"https://*.google-analytics.com",
"https://*.googletagmanager.com",
"*.fastly-insights.com",
"*.ethicalads.io",
],
"script-src": _script_src_settings(config),
"style-src": [
SELF,
"fonts.googleapis.com",
"*.ethicalads.io",
# Hashes for inline styles generated by v1.4.0 of ethicalads.min.js
"'sha256-2YHqZokjiizkHi1Zt+6ar0XJ0OeEy/egBnlm+MDMtrM='",
"'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='",
# Hashes for inline styles generated by v3.2.2 of MathJax tex-svg.js
"'sha256-JLEjeN9e5dGsz5475WyRaoA4eQOdNPxDIeUhclnJDCE='",
"'sha256-mQyxHEuwZJqpxCw3SLmc4YOySNKXunyu2Oiz1r3/wAE='",
"'sha256-OCf+kv5Asiwp++8PIevKBYSgnNLNUZvxAp4a7wMLuKA='",
"'sha256-h5LOiLhk6wiJrGsG5ItM0KimwzWQH/yAcmoJDJL//bY='",
],
"worker-src": ["*.fastly-insights.com"],
}
}
)
config.add_tween("warehouse.csp.content_security_policy_tween_factory")