##// END OF EJS Templates
hgweb: support Content Security Policy...
Gregory Szorc -
r30766:d7bf7d2b default
parent child Browse files
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
@@ -36,7 +36,7 b' navigate: <small class="navigate">{chang'
36 36 <ul id="graphnodes"></ul>
37 37 </div>
38 38
39 <script type="text/javascript">
39 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
40 40 <!-- hide script content
41 41
42 42 var data = {jsdata|json};
General Comments 0
You need to be logged in to leave comments. Login now