Show More
@@ -0,0 +1,129 b'' | |||
|
1 | #require serve | |
|
2 | ||
|
3 | $ cat > web.conf << EOF | |
|
4 | > [paths] | |
|
5 | > / = $TESTTMP/* | |
|
6 | > EOF | |
|
7 | ||
|
8 | $ hg init repo1 | |
|
9 | $ cd repo1 | |
|
10 | $ touch foo | |
|
11 | $ hg -q commit -A -m initial | |
|
12 | $ cd .. | |
|
13 | ||
|
14 | $ hg serve -p $HGPORT -d --pid-file=hg.pid --web-conf web.conf | |
|
15 | $ cat hg.pid >> $DAEMON_PIDS | |
|
16 | ||
|
17 | repo index should not send Content-Security-Policy header by default | |
|
18 | ||
|
19 | $ get-with-headers.py --headeronly localhost:$HGPORT '' content-security-policy etag | |
|
20 | 200 Script output follows | |
|
21 | ||
|
22 | static page should not send CSP by default | |
|
23 | ||
|
24 | $ get-with-headers.py --headeronly localhost:$HGPORT static/mercurial.js content-security-policy etag | |
|
25 | 200 Script output follows | |
|
26 | ||
|
27 | repo page should not send CSP by default, should send ETag | |
|
28 | ||
|
29 | $ get-with-headers.py --headeronly localhost:$HGPORT repo1 content-security-policy etag | |
|
30 | 200 Script output follows | |
|
31 | etag: W/"*" (glob) | |
|
32 | ||
|
33 | $ killdaemons.py | |
|
34 | ||
|
35 | Configure CSP without nonce | |
|
36 | ||
|
37 | $ cat >> web.conf << EOF | |
|
38 | > [web] | |
|
39 | > csp = script-src https://example.com/ 'unsafe-inline' | |
|
40 | > EOF | |
|
41 | ||
|
42 | $ hg serve -p $HGPORT -d --pid-file=hg.pid --web-conf web.conf | |
|
43 | $ cat hg.pid > $DAEMON_PIDS | |
|
44 | ||
|
45 | repo index should send Content-Security-Policy header when enabled | |
|
46 | ||
|
47 | $ get-with-headers.py --headeronly localhost:$HGPORT '' content-security-policy etag | |
|
48 | 200 Script output follows | |
|
49 | content-security-policy: script-src https://example.com/ 'unsafe-inline' | |
|
50 | ||
|
51 | static page should send CSP when enabled | |
|
52 | ||
|
53 | $ get-with-headers.py --headeronly localhost:$HGPORT static/mercurial.js content-security-policy etag | |
|
54 | 200 Script output follows | |
|
55 | content-security-policy: script-src https://example.com/ 'unsafe-inline' | |
|
56 | ||
|
57 | repo page should send CSP by default, include etag w/o nonce | |
|
58 | ||
|
59 | $ get-with-headers.py --headeronly localhost:$HGPORT repo1 content-security-policy etag | |
|
60 | 200 Script output follows | |
|
61 | content-security-policy: script-src https://example.com/ 'unsafe-inline' | |
|
62 | etag: W/"*" (glob) | |
|
63 | ||
|
64 | nonce should not be added to html if CSP doesn't use it | |
|
65 | ||
|
66 | $ get-with-headers.py localhost:$HGPORT repo1/graph/tip | egrep 'content-security-policy|<script' | |
|
67 | <script type="text/javascript" src="/repo1/static/mercurial.js"></script> | |
|
68 | <!--[if IE]><script type="text/javascript" src="/repo1/static/excanvas.js"></script><![endif]--> | |
|
69 | <script type="text/javascript"> | |
|
70 | <script type="text/javascript"> | |
|
71 | ||
|
72 | Configure CSP with nonce | |
|
73 | ||
|
74 | $ killdaemons.py | |
|
75 | $ cat >> web.conf << EOF | |
|
76 | > csp = image-src 'self'; script-src https://example.com/ 'nonce-%nonce%' | |
|
77 | > EOF | |
|
78 | ||
|
79 | $ hg serve -p $HGPORT -d --pid-file=hg.pid --web-conf web.conf | |
|
80 | $ cat hg.pid > $DAEMON_PIDS | |
|
81 | ||
|
82 | nonce should be substituted in CSP header | |
|
83 | ||
|
84 | $ get-with-headers.py --headeronly localhost:$HGPORT '' content-security-policy etag | |
|
85 | 200 Script output follows | |
|
86 | content-security-policy: image-src 'self'; script-src https://example.com/ 'nonce-*' (glob) | |
|
87 | ||
|
88 | nonce should be included in CSP for static pages | |
|
89 | ||
|
90 | $ get-with-headers.py --headeronly localhost:$HGPORT static/mercurial.js content-security-policy etag | |
|
91 | 200 Script output follows | |
|
92 | content-security-policy: image-src 'self'; script-src https://example.com/ 'nonce-*' (glob) | |
|
93 | ||
|
94 | repo page should have nonce, no ETag | |
|
95 | ||
|
96 | $ get-with-headers.py --headeronly localhost:$HGPORT repo1 content-security-policy etag | |
|
97 | 200 Script output follows | |
|
98 | content-security-policy: image-src 'self'; script-src https://example.com/ 'nonce-*' (glob) | |
|
99 | ||
|
100 | nonce should be added to html when used | |
|
101 | ||
|
102 | $ get-with-headers.py localhost:$HGPORT repo1/graph/tip content-security-policy | egrep 'content-security-policy|<script' | |
|
103 | content-security-policy: image-src 'self'; script-src https://example.com/ 'nonce-*' (glob) | |
|
104 | <script type="text/javascript" src="/repo1/static/mercurial.js"></script> | |
|
105 | <!--[if IE]><script type="text/javascript" src="/repo1/static/excanvas.js"></script><![endif]--> | |
|
106 | <script type="text/javascript" nonce="*"> (glob) | |
|
107 | <script type="text/javascript" nonce="*"> (glob) | |
|
108 | ||
|
109 | hgweb_mod w/o hgwebdir works as expected | |
|
110 | ||
|
111 | $ killdaemons.py | |
|
112 | ||
|
113 | $ hg -R repo1 serve -p $HGPORT -d --pid-file=hg.pid --config "web.csp=image-src 'self'; script-src https://example.com/ 'nonce-%nonce%'" | |
|
114 | $ cat hg.pid > $DAEMON_PIDS | |
|
115 | ||
|
116 | static page sends CSP | |
|
117 | ||
|
118 | $ get-with-headers.py --headeronly localhost:$HGPORT static/mercurial.js content-security-policy etag | |
|
119 | 200 Script output follows | |
|
120 | content-security-policy: image-src 'self'; script-src https://example.com/ 'nonce-*' (glob) | |
|
121 | ||
|
122 | nonce included in <script> and headers | |
|
123 | ||
|
124 | $ get-with-headers.py localhost:$HGPORT graph/tip content-security-policy | egrep 'content-security-policy|<script' | |
|
125 | content-security-policy: image-src 'self'; script-src https://example.com/ 'nonce-*' (glob) | |
|
126 | <script type="text/javascript" src="/static/mercurial.js"></script> | |
|
127 | <!--[if IE]><script type="text/javascript" src="/static/excanvas.js"></script><![endif]--> | |
|
128 | <script type="text/javascript" nonce="*"> (glob) | |
|
129 | <script type="text/javascript" nonce="*"> (glob) |
@@ -2111,6 +2111,20 b' The full set of options is:' | |||
|
2111 | 2111 | Name or email address of the person in charge of the repository. |
|
2112 | 2112 | (default: ui.username or ``$EMAIL`` or "unknown" if unset or empty) |
|
2113 | 2113 | |
|
2114 | ``csp`` | |
|
2115 | Send a ``Content-Security-Policy`` HTTP header with this value. | |
|
2116 | ||
|
2117 | The value may contain a special string ``%nonce%``, which will be replaced | |
|
2118 | by a randomly-generated one-time use value. If the value contains | |
|
2119 | ``%nonce%``, ``web.cache`` will be disabled, as caching undermines the | |
|
2120 | one-time property of the nonce. This nonce will also be inserted into | |
|
2121 | ``<script>`` elements containing inline JavaScript. | |
|
2122 | ||
|
2123 | Note: lots of HTML content sent by the server is derived from repository | |
|
2124 | data. Please consider the potential for malicious repository data to | |
|
2125 | "inject" itself into generated HTML content as part of your security | |
|
2126 | threat model. | |
|
2127 | ||
|
2114 | 2128 | ``deny_push`` |
|
2115 | 2129 | Whether to deny pushing to the repository. If empty or not set, |
|
2116 | 2130 | push is not denied. If the special value ``*``, all remote users are |
@@ -8,9 +8,11 b'' | |||
|
8 | 8 | |
|
9 | 9 | from __future__ import absolute_import |
|
10 | 10 | |
|
11 | import base64 | |
|
11 | 12 | import errno |
|
12 | 13 | import mimetypes |
|
13 | 14 | import os |
|
15 | import uuid | |
|
14 | 16 | |
|
15 | 17 | from .. import ( |
|
16 | 18 | encoding, |
@@ -199,3 +201,22 b' def caching(web, req):' | |||
|
199 | 201 | if req.env.get('HTTP_IF_NONE_MATCH') == tag: |
|
200 | 202 | raise ErrorResponse(HTTP_NOT_MODIFIED) |
|
201 | 203 | req.headers.append(('ETag', tag)) |
|
204 | ||
|
205 | def cspvalues(ui): | |
|
206 | """Obtain the Content-Security-Policy header and nonce value. | |
|
207 | ||
|
208 | Returns a 2-tuple of the CSP header value and the nonce value. | |
|
209 | ||
|
210 | First value is ``None`` if CSP isn't enabled. Second value is ``None`` | |
|
211 | if CSP isn't enabled or if the CSP header doesn't need a nonce. | |
|
212 | """ | |
|
213 | # Don't allow untrusted CSP setting since it be disable protections | |
|
214 | # from a trusted/global source. | |
|
215 | csp = ui.config('web', 'csp', untrusted=False) | |
|
216 | nonce = None | |
|
217 | ||
|
218 | if csp and '%nonce%' in csp: | |
|
219 | nonce = base64.urlsafe_b64encode(uuid.uuid4().bytes).rstrip('=') | |
|
220 | csp = csp.replace('%nonce%', nonce) | |
|
221 | ||
|
222 | return csp, nonce |
@@ -19,6 +19,7 b' from .common import (' | |||
|
19 | 19 | HTTP_OK, |
|
20 | 20 | HTTP_SERVER_ERROR, |
|
21 | 21 | caching, |
|
22 | cspvalues, | |
|
22 | 23 | permhooks, |
|
23 | 24 | ) |
|
24 | 25 | from .request import wsgirequest |
@@ -115,6 +116,8 b' class requestcontext(object):' | |||
|
115 | 116 | # of the request. |
|
116 | 117 | self.websubtable = app.websubtable |
|
117 | 118 | |
|
119 | self.csp, self.nonce = cspvalues(self.repo.ui) | |
|
120 | ||
|
118 | 121 | # Trust the settings from the .hg/hgrc files by default. |
|
119 | 122 | def config(self, section, name, default=None, untrusted=True): |
|
120 | 123 | return self.repo.ui.config(section, name, default, |
@@ -201,6 +204,7 b' class requestcontext(object):' | |||
|
201 | 204 | 'sessionvars': sessionvars, |
|
202 | 205 | 'pathdef': makebreadcrumb(req.url), |
|
203 | 206 | 'style': style, |
|
207 | 'nonce': self.nonce, | |
|
204 | 208 | } |
|
205 | 209 | tmpl = templater.templater.frommapfile(mapfile, |
|
206 | 210 | filters={'websub': websubfilter}, |
@@ -318,6 +322,13 b' class hgweb(object):' | |||
|
318 | 322 | encoding.encoding = rctx.config('web', 'encoding', encoding.encoding) |
|
319 | 323 | rctx.repo.ui.environ = req.env |
|
320 | 324 | |
|
325 | if rctx.csp: | |
|
326 | # hgwebdir may have added CSP header. Since we generate our own, | |
|
327 | # replace it. | |
|
328 | req.headers = [h for h in req.headers | |
|
329 | if h[0] != 'Content-Security-Policy'] | |
|
330 | req.headers.append(('Content-Security-Policy', rctx.csp)) | |
|
331 | ||
|
321 | 332 | # work with CGI variables to create coherent structure |
|
322 | 333 | # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME |
|
323 | 334 | |
@@ -414,7 +425,9 b' class hgweb(object):' | |||
|
414 | 425 | req.form['cmd'] = [tmpl.cache['default']] |
|
415 | 426 | cmd = req.form['cmd'][0] |
|
416 | 427 | |
|
417 | if rctx.configbool('web', 'cache', True): | |
|
428 | # Don't enable caching if using a CSP nonce because then it wouldn't | |
|
429 | # be a nonce. | |
|
430 | if rctx.configbool('web', 'cache', True) and not rctx.nonce: | |
|
418 | 431 | caching(self, req) # sets ETag header or raises NOT_MODIFIED |
|
419 | 432 | if cmd not in webcommands.__all__: |
|
420 | 433 | msg = 'no such method: %s' % cmd |
@@ -19,6 +19,7 b' from .common import (' | |||
|
19 | 19 | HTTP_NOT_FOUND, |
|
20 | 20 | HTTP_OK, |
|
21 | 21 | HTTP_SERVER_ERROR, |
|
22 | cspvalues, | |
|
22 | 23 | get_contact, |
|
23 | 24 | get_mtime, |
|
24 | 25 | ismember, |
@@ -227,8 +228,12 b' class hgwebdir(object):' | |||
|
227 | 228 | try: |
|
228 | 229 | self.refresh() |
|
229 | 230 | |
|
231 | csp, nonce = cspvalues(self.ui) | |
|
232 | if csp: | |
|
233 | req.headers.append(('Content-Security-Policy', csp)) | |
|
234 | ||
|
230 | 235 | virtual = req.env.get("PATH_INFO", "").strip('/') |
|
231 | tmpl = self.templater(req) | |
|
236 | tmpl = self.templater(req, nonce) | |
|
232 | 237 | ctype = tmpl('mimetype', encoding=encoding.encoding) |
|
233 | 238 | ctype = templater.stringify(ctype) |
|
234 | 239 | |
@@ -466,7 +471,7 b' class hgwebdir(object):' | |||
|
466 | 471 | sortcolumn=sortcolumn, descending=descending, |
|
467 | 472 | **dict(sort)) |
|
468 | 473 | |
|
469 | def templater(self, req): | |
|
474 | def templater(self, req, nonce): | |
|
470 | 475 | |
|
471 | 476 | def motd(**map): |
|
472 | 477 | if self.motd is not None: |
@@ -510,6 +515,7 b' class hgwebdir(object):' | |||
|
510 | 515 | "staticurl": staticurl, |
|
511 | 516 | "sessionvars": sessionvars, |
|
512 | 517 | "style": style, |
|
518 | "nonce": nonce, | |
|
513 | 519 | } |
|
514 | 520 | tmpl = templater.templater.frommapfile(mapfile, defaults=defaults) |
|
515 | 521 | return tmpl |
@@ -45,7 +45,7 b' graph |' | |||
|
45 | 45 | <ul id="graphnodes"></ul> |
|
46 | 46 | </div> |
|
47 | 47 | |
|
48 | <script> | |
|
48 | <script{if(nonce, ' nonce="{nonce}"')}> | |
|
49 | 49 | <!-- hide script content |
|
50 | 50 | |
|
51 | 51 | var data = {jsdata|json}; |
@@ -108,7 +108,7 b' graph.render(data);' | |||
|
108 | 108 | | {changenav%navgraph} |
|
109 | 109 | </div> |
|
110 | 110 | |
|
111 | <script type="text/javascript"> | |
|
111 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
112 | 112 | ajaxScrollInit( |
|
113 | 113 | '{url|urlescape}graph/{rev}?revcount=%next%&style={style}', |
|
114 | 114 | {revcount}+60, |
@@ -40,7 +40,7 b' shortlog |' | |||
|
40 | 40 | {changenav%navshort} |
|
41 | 41 | </div> |
|
42 | 42 | |
|
43 | <script type="text/javascript"> | |
|
43 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
44 | 44 | ajaxScrollInit( |
|
45 | 45 | '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}', |
|
46 | 46 | '{nextentry%"{node}"}', <!-- NEXTHASH |
@@ -40,7 +40,7 b'' | |||
|
40 | 40 | <ul id="graphnodes"></ul> |
|
41 | 41 | </div> |
|
42 | 42 | |
|
43 | <script> | |
|
43 | <script{if(nonce, ' nonce="{nonce}"')}> | |
|
44 | 44 | <!-- hide script content |
|
45 | 45 | |
|
46 | 46 | document.getElementById('noscript').style.display = 'none'; |
@@ -104,7 +104,7 b'' | |||
|
104 | 104 | | {changenav%navgraph} |
|
105 | 105 | </div> |
|
106 | 106 | |
|
107 | <script type="text/javascript"> | |
|
107 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
108 | 108 | ajaxScrollInit( |
|
109 | 109 | '{url|urlescape}graph/{rev}?revcount=%next%&style={style}', |
|
110 | 110 | {revcount}+60, |
@@ -41,7 +41,7 b'' | |||
|
41 | 41 | {changenav%navshort} |
|
42 | 42 | </div> |
|
43 | 43 | |
|
44 | <script type="text/javascript"> | |
|
44 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
45 | 45 | ajaxScrollInit( |
|
46 | 46 | '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}', |
|
47 | 47 | '{nextentry%"{node}"}', <!-- NEXTHASH |
@@ -59,7 +59,7 b'' | |||
|
59 | 59 | <ul id="graphnodes"></ul> |
|
60 | 60 | </div> |
|
61 | 61 | |
|
62 | <script type="text/javascript"> | |
|
62 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
63 | 63 | <!-- hide script content |
|
64 | 64 | |
|
65 | 65 | var data = {jsdata|json}; |
@@ -121,7 +121,7 b' graph.render(data);' | |||
|
121 | 121 | | rev {rev}: {changenav%navgraph} |
|
122 | 122 | </div> |
|
123 | 123 | |
|
124 | <script type="text/javascript"> | |
|
124 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
125 | 125 | ajaxScrollInit( |
|
126 | 126 | '{url|urlescape}graph/{rev}?revcount=%next%&style={style}', |
|
127 | 127 | {revcount}+60, |
@@ -72,7 +72,7 b'' | |||
|
72 | 72 | | rev {rev}: {changenav%navshort} |
|
73 | 73 | </div> |
|
74 | 74 | |
|
75 | <script type="text/javascript"> | |
|
75 | <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}> | |
|
76 | 76 | ajaxScrollInit( |
|
77 | 77 | '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}', |
|
78 | 78 | '{nextentry%"{node}"}', <!-- NEXTHASH |
General Comments 0
You need to be logged in to leave comments.
Login now