##// 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 Name or email address of the person in charge of the repository.
2111 Name or email address of the person in charge of the repository.
2112 (default: ui.username or ``$EMAIL`` or "unknown" if unset or empty)
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 ``deny_push``
2128 ``deny_push``
2115 Whether to deny pushing to the repository. If empty or not set,
2129 Whether to deny pushing to the repository. If empty or not set,
2116 push is not denied. If the special value ``*``, all remote users are
2130 push is not denied. If the special value ``*``, all remote users are
@@ -8,9 +8,11 b''
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import base64
11 import errno
12 import errno
12 import mimetypes
13 import mimetypes
13 import os
14 import os
15 import uuid
14
16
15 from .. import (
17 from .. import (
16 encoding,
18 encoding,
@@ -199,3 +201,22 b' def caching(web, req):'
199 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
201 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
200 raise ErrorResponse(HTTP_NOT_MODIFIED)
202 raise ErrorResponse(HTTP_NOT_MODIFIED)
201 req.headers.append(('ETag', tag))
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 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 caching,
21 caching,
22 cspvalues,
22 permhooks,
23 permhooks,
23 )
24 )
24 from .request import wsgirequest
25 from .request import wsgirequest
@@ -115,6 +116,8 b' class requestcontext(object):'
115 # of the request.
116 # of the request.
116 self.websubtable = app.websubtable
117 self.websubtable = app.websubtable
117
118
119 self.csp, self.nonce = cspvalues(self.repo.ui)
120
118 # Trust the settings from the .hg/hgrc files by default.
121 # Trust the settings from the .hg/hgrc files by default.
119 def config(self, section, name, default=None, untrusted=True):
122 def config(self, section, name, default=None, untrusted=True):
120 return self.repo.ui.config(section, name, default,
123 return self.repo.ui.config(section, name, default,
@@ -201,6 +204,7 b' class requestcontext(object):'
201 'sessionvars': sessionvars,
204 'sessionvars': sessionvars,
202 'pathdef': makebreadcrumb(req.url),
205 'pathdef': makebreadcrumb(req.url),
203 'style': style,
206 'style': style,
207 'nonce': self.nonce,
204 }
208 }
205 tmpl = templater.templater.frommapfile(mapfile,
209 tmpl = templater.templater.frommapfile(mapfile,
206 filters={'websub': websubfilter},
210 filters={'websub': websubfilter},
@@ -318,6 +322,13 b' class hgweb(object):'
318 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
322 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
319 rctx.repo.ui.environ = req.env
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 # work with CGI variables to create coherent structure
332 # work with CGI variables to create coherent structure
322 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
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 req.form['cmd'] = [tmpl.cache['default']]
425 req.form['cmd'] = [tmpl.cache['default']]
415 cmd = req.form['cmd'][0]
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 caching(self, req) # sets ETag header or raises NOT_MODIFIED
431 caching(self, req) # sets ETag header or raises NOT_MODIFIED
419 if cmd not in webcommands.__all__:
432 if cmd not in webcommands.__all__:
420 msg = 'no such method: %s' % cmd
433 msg = 'no such method: %s' % cmd
@@ -19,6 +19,7 b' from .common import ('
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 cspvalues,
22 get_contact,
23 get_contact,
23 get_mtime,
24 get_mtime,
24 ismember,
25 ismember,
@@ -227,8 +228,12 b' class hgwebdir(object):'
227 try:
228 try:
228 self.refresh()
229 self.refresh()
229
230
231 csp, nonce = cspvalues(self.ui)
232 if csp:
233 req.headers.append(('Content-Security-Policy', csp))
234
230 virtual = req.env.get("PATH_INFO", "").strip('/')
235 virtual = req.env.get("PATH_INFO", "").strip('/')
231 tmpl = self.templater(req)
236 tmpl = self.templater(req, nonce)
232 ctype = tmpl('mimetype', encoding=encoding.encoding)
237 ctype = tmpl('mimetype', encoding=encoding.encoding)
233 ctype = templater.stringify(ctype)
238 ctype = templater.stringify(ctype)
234
239
@@ -466,7 +471,7 b' class hgwebdir(object):'
466 sortcolumn=sortcolumn, descending=descending,
471 sortcolumn=sortcolumn, descending=descending,
467 **dict(sort))
472 **dict(sort))
468
473
469 def templater(self, req):
474 def templater(self, req, nonce):
470
475
471 def motd(**map):
476 def motd(**map):
472 if self.motd is not None:
477 if self.motd is not None:
@@ -510,6 +515,7 b' class hgwebdir(object):'
510 "staticurl": staticurl,
515 "staticurl": staticurl,
511 "sessionvars": sessionvars,
516 "sessionvars": sessionvars,
512 "style": style,
517 "style": style,
518 "nonce": nonce,
513 }
519 }
514 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
520 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
515 return tmpl
521 return tmpl
@@ -45,7 +45,7 b' graph |'
45 <ul id="graphnodes"></ul>
45 <ul id="graphnodes"></ul>
46 </div>
46 </div>
47
47
48 <script>
48 <script{if(nonce, ' nonce="{nonce}"')}>
49 <!-- hide script content
49 <!-- hide script content
50
50
51 var data = {jsdata|json};
51 var data = {jsdata|json};
@@ -108,7 +108,7 b' graph.render(data);'
108 | {changenav%navgraph}
108 | {changenav%navgraph}
109 </div>
109 </div>
110
110
111 <script type="text/javascript">
111 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
112 ajaxScrollInit(
112 ajaxScrollInit(
113 '{url|urlescape}graph/{rev}?revcount=%next%&style={style}',
113 '{url|urlescape}graph/{rev}?revcount=%next%&style={style}',
114 {revcount}+60,
114 {revcount}+60,
@@ -40,7 +40,7 b' shortlog |'
40 {changenav%navshort}
40 {changenav%navshort}
41 </div>
41 </div>
42
42
43 <script type="text/javascript">
43 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
44 ajaxScrollInit(
44 ajaxScrollInit(
45 '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}',
45 '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}',
46 '{nextentry%"{node}"}', <!-- NEXTHASH
46 '{nextentry%"{node}"}', <!-- NEXTHASH
@@ -40,7 +40,7 b''
40 <ul id="graphnodes"></ul>
40 <ul id="graphnodes"></ul>
41 </div>
41 </div>
42
42
43 <script>
43 <script{if(nonce, ' nonce="{nonce}"')}>
44 <!-- hide script content
44 <!-- hide script content
45
45
46 document.getElementById('noscript').style.display = 'none';
46 document.getElementById('noscript').style.display = 'none';
@@ -104,7 +104,7 b''
104 | {changenav%navgraph}
104 | {changenav%navgraph}
105 </div>
105 </div>
106
106
107 <script type="text/javascript">
107 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
108 ajaxScrollInit(
108 ajaxScrollInit(
109 '{url|urlescape}graph/{rev}?revcount=%next%&style={style}',
109 '{url|urlescape}graph/{rev}?revcount=%next%&style={style}',
110 {revcount}+60,
110 {revcount}+60,
@@ -41,7 +41,7 b''
41 {changenav%navshort}
41 {changenav%navshort}
42 </div>
42 </div>
43
43
44 <script type="text/javascript">
44 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
45 ajaxScrollInit(
45 ajaxScrollInit(
46 '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}',
46 '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}',
47 '{nextentry%"{node}"}', <!-- NEXTHASH
47 '{nextentry%"{node}"}', <!-- NEXTHASH
@@ -59,7 +59,7 b''
59 <ul id="graphnodes"></ul>
59 <ul id="graphnodes"></ul>
60 </div>
60 </div>
61
61
62 <script type="text/javascript">
62 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
63 <!-- hide script content
63 <!-- hide script content
64
64
65 var data = {jsdata|json};
65 var data = {jsdata|json};
@@ -121,7 +121,7 b' graph.render(data);'
121 | rev {rev}: {changenav%navgraph}
121 | rev {rev}: {changenav%navgraph}
122 </div>
122 </div>
123
123
124 <script type="text/javascript">
124 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
125 ajaxScrollInit(
125 ajaxScrollInit(
126 '{url|urlescape}graph/{rev}?revcount=%next%&style={style}',
126 '{url|urlescape}graph/{rev}?revcount=%next%&style={style}',
127 {revcount}+60,
127 {revcount}+60,
@@ -72,7 +72,7 b''
72 | rev {rev}: {changenav%navshort}
72 | rev {rev}: {changenav%navshort}
73 </div>
73 </div>
74
74
75 <script type="text/javascript">
75 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
76 ajaxScrollInit(
76 ajaxScrollInit(
77 '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}',
77 '{url|urlescape}shortlog/%next%{sessionvars%urlparameter}',
78 '{nextentry%"{node}"}', <!-- NEXTHASH
78 '{nextentry%"{node}"}', <!-- NEXTHASH
@@ -36,7 +36,7 b' navigate: <small class="navigate">{chang'
36 <ul id="graphnodes"></ul>
36 <ul id="graphnodes"></ul>
37 </div>
37 </div>
38
38
39 <script type="text/javascript">
39 <script type="text/javascript"{if(nonce, ' nonce="{nonce}"')}>
40 <!-- hide script content
40 <!-- hide script content
41
41
42 var data = {jsdata|json};
42 var data = {jsdata|json};
General Comments 0
You need to be logged in to leave comments. Login now