comparison test/test_cgi.py @ 5201:a9ace22e0a2f

issue 2550690 - Adding anti-csrf measures to roundup following https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet and https://seclab.stanford.edu/websec/csrf/csrf.pdf Basically implement Synchronizer (CSRF) Tokens per form on a page. Single use (destroyed once used). Random input data for the token includes: system random implementation in python using /dev/urandom (fallback to random based on timestamp as the seed. Not as good, but should be ok for the short lifetime of the token??) the id (in cpython it's the memory address) of the object requesting a token. In theory this depends on memory layout, the history of the process (how many previous objects have been allocated from the heap etc.) I claim without any proof that for long running processes this is another source of randomness. For short running processes with little activity it could be guessed. last the floating point time.time() value is added. This may only have 1 second resolution so may be guessable. Hopefully for a short lived (2 week by default) token this is sufficient. Also in the current implementation the user is notified when validation fails and is told why. This allows the roundup admin to find the log entry (at error level) and try to resolve the issue. In the future user notification may change but for now this is probably best.
author John Rouillard <rouilj@ieee.org>
date Sat, 18 Mar 2017 16:59:01 -0400
parents 349bef975367
children 9f490cc0effe
comparison
equal deleted inserted replaced
5200:16a8a3f0772c 5201:a9ace22e0a2f
119 # compile the labels re 119 # compile the labels re
120 classes = '|'.join(self.db.classes.keys()) 120 classes = '|'.join(self.db.classes.keys())
121 self.FV_SPECIAL = re.compile(FormParser.FV_LABELS%classes, 121 self.FV_SPECIAL = re.compile(FormParser.FV_LABELS%classes,
122 re.VERBOSE) 122 re.VERBOSE)
123 123
124 def setupClient(self, form, classname, nodeid=None, template='item'): 124 def setupClient(self, form, classname, nodeid=None, template='item', env_addon=None):
125 cl = client.Client(self.instance, None, {'PATH_INFO':'/', 125 cl = client.Client(self.instance, None, {'PATH_INFO':'/',
126 'REQUEST_METHOD':'POST'}, makeForm(form)) 126 'REQUEST_METHOD':'POST'}, makeForm(form))
127 cl.classname = classname 127 cl.classname = classname
128 cl.base = 'http://whoami.com/path/'
128 cl.nodeid = nodeid 129 cl.nodeid = nodeid
129 cl.language = ('en',) 130 cl.language = ('en',)
130 cl.userid = '1' 131 cl.userid = '1'
131 cl.db = self.db 132 cl.db = self.db
132 cl.user = 'admin' 133 cl.user = 'admin'
133 cl.template = template 134 cl.template = template
135 if env_addon is not None:
136 cl.env.update(env_addon)
134 return cl 137 return cl
135 138
136 def parseForm(self, form, classname='test', nodeid=None): 139 def parseForm(self, form, classname='test', nodeid=None):
137 cl = self.setupClient(form, classname, nodeid) 140 cl = self.setupClient(form, classname, nodeid)
138 return cl.parsePropsFromForm(create=1) 141 return cl.parsePropsFromForm(create=1)
805 status = '1', nosy = ['1'], keyword = ['1']) 808 status = '1', nosy = ['1'], keyword = ['1'])
806 self.db.commit () 809 self.db.commit ()
807 form = {':note': 'msg-content', 'title': 'New title', 810 form = {':note': 'msg-content', 'title': 'New title',
808 'priority': '2', 'status': '2', 'nosy': '1,2', 'keyword': '', 811 'priority': '2', 'status': '2', 'nosy': '1,2', 'keyword': '',
809 'superseder': '5000', ':action': 'edit'} 812 'superseder': '5000', ':action': 'edit'}
810 cl = self.setupClient(form, 'issue', '1') 813 cl = self.setupClient(form, 'issue', '1',
814 env_addon = {'HTTP_REFERER': 'http://whoami.com/path/'})
811 pt = RoundupPageTemplate() 815 pt = RoundupPageTemplate()
812 pt.pt_edit(page_template, 'text/html') 816 pt.pt_edit(page_template, 'text/html')
813 out = [] 817 out = []
814 def wh(s): 818 def wh(s):
815 out.append(s) 819 out.append(s)
851 <p><input type="text" name="superseder" value="5000" size="30"></p> 855 <p><input type="text" name="superseder" value="5000" size="30"></p>
852 </body> 856 </body>
853 </html> 857 </html>
854 """.strip ()) 858 """.strip ())
855 859
860 def testCsrfHeaderProtection(self):
861 # need to set SENDMAILDEBUG to prevent
862 # downstream issue when email is sent on successful
863 # issue creation. Also delete the file afterwards
864 # just tomake sure that someother test looking for
865 # SENDMAILDEBUG won't trip over ours.
866 if not os.environ.has_key('SENDMAILDEBUG'):
867 os.environ['SENDMAILDEBUG'] = 'mail-test1.log'
868 SENDMAILDEBUG = os.environ['SENDMAILDEBUG']
869
870 page_template = """
871 <html>
872 <body>
873 <p tal:condition="options/error_message|nothing"
874 tal:repeat="m options/error_message"
875 tal:content="structure m"/>
876 <p tal:content="context/title/plain"/>
877 <p tal:content="context/priority/plain"/>
878 <p tal:content="context/status/plain"/>
879 <p tal:content="context/nosy/plain"/>
880 <p tal:content="context/keyword/plain"/>
881 <p tal:content="structure context/superseder/field"/>
882 </body>
883 </html>
884 """.strip ()
885 self.db.keyword.create (name = 'key1')
886 self.db.keyword.create (name = 'key2')
887 nodeid = self.db.issue.create (title = 'Title', priority = '1',
888 status = '1', nosy = ['1'], keyword = ['1'])
889 self.db.commit ()
890 form = {':note': 'msg-content', 'title': 'New title',
891 'priority': '2', 'status': '2', 'nosy': '1,2', 'keyword': '',
892 ':action': 'edit'}
893 cl = self.setupClient(form, 'issue', '1')
894 pt = RoundupPageTemplate()
895 pt.pt_edit(page_template, 'text/html')
896 out = []
897 print "out1: ", id(out), out
898 def wh(s):
899 out.append(s)
900 cl.write_html = wh
901 # Enable the following if we get a templating error:
902 #def send_error (*args, **kw):
903 # import pdb; pdb.set_trace()
904 #cl.send_error_to_admin = send_error
905 # Need to rollback the database on error -- this usually happens
906 # in web-interface (and for other databases) anyway, need it for
907 # testing that the form values are really used, not the database!
908 # We do this together with the setup of the easy template above
909 def load_template(x):
910 cl.db.rollback()
911 return pt
912 cl.instance.templates.load = load_template
913 cl.selectTemplate = MockNull()
914 cl.determine_context = MockNull ()
915 def hasPermission(s, p, classname=None, d=None, e=None, **kw):
916 return True
917 actions.Action.hasPermission = hasPermission
918 e1 = _HTMLItem.is_edit_ok
919 _HTMLItem.is_edit_ok = lambda x : True
920 e2 = HTMLProperty.is_edit_ok
921 HTMLProperty.is_edit_ok = lambda x : True
922
923 # test with no headers and config by default requires 1
924 cl.inner_main()
925 match_at=out[0].find('Unable to verify sufficient headers')
926 print out[0]
927 self.assertNotEqual(match_at, -1)
928 del(out[0])
929
930 # all the rest of these allow at least one header to pass
931 # and the edit happens with a redirect back to issue 1
932 cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
933 cl.inner_main()
934 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
935 self.assertEqual(match_at, 0)
936 del(cl.env['HTTP_REFERER'])
937 del(out[0])
938
939 cl.env['HTTP_ORIGIN'] = 'http://whoami.com'
940 cl.inner_main()
941 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
942 self.assertEqual(match_at, 0)
943 del(cl.env['HTTP_ORIGIN'])
944 del(out[0])
945
946 cl.env['HTTP_X-FORWARDED-HOST'] = 'whoami.com'
947 # if there is an X-FORWARDED-HOST header it is used and
948 # HOST header is ignored. X-FORWARDED-HOST should only be
949 # passed/set by a proxy. In this case the HOST header is
950 # the proxy's name for the web server and not the name
951 # thatis exposed to the world.
952 cl.env['HTTP_HOST'] = 'frontend1.whoami.net'
953 cl.inner_main()
954 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
955 self.assertNotEqual(match_at, -1)
956 del(cl.env['HTTP_X-FORWARDED-HOST'])
957 del(cl.env['HTTP_HOST'])
958 del(out[0])
959
960 cl.env['HTTP_HOST'] = 'whoami.com'
961 cl.inner_main()
962 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
963 self.assertEqual(match_at, 0)
964 del(cl.env['HTTP_HOST'])
965 del(out[0])
966
967 # try failing headers
968 cl.env['HTTP_X-FORWARDED-HOST'] = 'whoami.net'
969 # this raises an error as the header check passes and
970 # it did the edit and tries to send mail.
971 cl.inner_main()
972 match_at=out[0].find('Invalid X-FORWARDED-HOST whoami.net')
973 self.assertNotEqual(match_at, -1)
974 del(out[0])
975
976 # clean up from email log
977 if os.path.exists(SENDMAILDEBUG):
978 os.remove(SENDMAILDEBUG)
979 #raise ValueError
980
856 # 981 #
857 # SECURITY 982 # SECURITY
858 # 983 #
859 # XXX test all default permissions 984 # XXX test all default permissions
860 def _make_client(self, form, classname='user', nodeid='1', 985 def _make_client(self, form, classname='user', nodeid='1',
1279 1404
1280 # Test ok state template that uses user.forgotten.html 1405 # Test ok state template that uses user.forgotten.html
1281 self.client.form=makeForm({"@template": "forgotten|item"}) 1406 self.client.form=makeForm({"@template": "forgotten|item"})
1282 self.client.path = 'user' 1407 self.client.path = 'user'
1283 self.client.determine_context() 1408 self.client.determine_context()
1409 self.client.session_api = MockNull(_sid="1234567890")
1284 self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), ('user', 'forgotten|item', None)) 1410 self.assertEqual((self.client.classname, self.client.template, self.client.nodeid), ('user', 'forgotten|item', None))
1285 self.assertEqual(self.client._ok_message, []) 1411 self.assertEqual(self.client._ok_message, [])
1286 1412
1287 result = self.client.renderContext() 1413 result = self.client.renderContext()
1288 self.assertNotEqual(-1, 1414 self.assertNotEqual(-1,

Roundup Issue Tracker: http://roundup-tracker.org/