comparison test/test_cgi.py @ 5220:14d8f61e6ef2

Reimplemented anti-csrf measures by raising exceptions rather than returning booleans. Redoing it using exceptions was the easiest way to return proper xmlrpc fault messages to the clients. Also this code should now properly make values set in the form override values from the database. So no lost work under some circumstances if the csrf requirements are not met. Also this code does a better job of cleaning up old csrf tokens.
author John Rouillard <rouilj@ieee.org>
date Wed, 05 Apr 2017 20:56:08 -0400
parents 44f7e6b958fe
children 8743b7226dc7
comparison
equal deleted inserted replaced
5219:ade4bbc2716d 5220:14d8f61e6ef2
921 HTMLProperty.is_edit_ok = lambda x : True 921 HTMLProperty.is_edit_ok = lambda x : True
922 922
923 # test with no headers and config by default requires 1 923 # test with no headers and config by default requires 1
924 cl.inner_main() 924 cl.inner_main()
925 match_at=out[0].find('Unable to verify sufficient headers') 925 match_at=out[0].find('Unable to verify sufficient headers')
926 print "result of subtest 1:", out[0]
926 self.assertNotEqual(match_at, -1) 927 self.assertNotEqual(match_at, -1)
927 del(out[0]) 928 del(out[0])
928 929
929 # all the rest of these allow at least one header to pass 930 # all the rest of these allow at least one header to pass
930 # and the edit happens with a redirect back to issue 1 931 # and the edit happens with a redirect back to issue 1
931 cl.env['HTTP_REFERER'] = 'http://whoami.com/path/' 932 cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
932 cl.inner_main() 933 cl.inner_main()
933 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message') 934 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
935 print "result of subtest 2:", out[0]
934 self.assertEqual(match_at, 0) 936 self.assertEqual(match_at, 0)
935 del(cl.env['HTTP_REFERER']) 937 del(cl.env['HTTP_REFERER'])
936 del(out[0]) 938 del(out[0])
937 939
938 cl.env['HTTP_ORIGIN'] = 'http://whoami.com' 940 cl.env['HTTP_ORIGIN'] = 'http://whoami.com'
939 cl.inner_main() 941 cl.inner_main()
940 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message') 942 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
943 print "result of subtest 3:", out[0]
941 self.assertEqual(match_at, 0) 944 self.assertEqual(match_at, 0)
942 del(cl.env['HTTP_ORIGIN']) 945 del(cl.env['HTTP_ORIGIN'])
943 del(out[0]) 946 del(out[0])
944 947
945 cl.env['HTTP_X-FORWARDED-HOST'] = 'whoami.com' 948 cl.env['HTTP_X-FORWARDED-HOST'] = 'whoami.com'
949 # the proxy's name for the web server and not the name 952 # the proxy's name for the web server and not the name
950 # thatis exposed to the world. 953 # thatis exposed to the world.
951 cl.env['HTTP_HOST'] = 'frontend1.whoami.net' 954 cl.env['HTTP_HOST'] = 'frontend1.whoami.net'
952 cl.inner_main() 955 cl.inner_main()
953 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message') 956 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
957 print "result of subtest 4:", out[0]
954 self.assertNotEqual(match_at, -1) 958 self.assertNotEqual(match_at, -1)
955 del(cl.env['HTTP_X-FORWARDED-HOST']) 959 del(cl.env['HTTP_X-FORWARDED-HOST'])
956 del(cl.env['HTTP_HOST']) 960 del(cl.env['HTTP_HOST'])
957 del(out[0]) 961 del(out[0])
958 962
959 cl.env['HTTP_HOST'] = 'whoami.com' 963 cl.env['HTTP_HOST'] = 'whoami.com'
960 cl.inner_main() 964 cl.inner_main()
961 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message') 965 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
966 print "result of subtest 5:", out[0]
962 self.assertEqual(match_at, 0) 967 self.assertEqual(match_at, 0)
963 del(cl.env['HTTP_HOST']) 968 del(cl.env['HTTP_HOST'])
964 del(out[0]) 969 del(out[0])
965 970
966 # try failing headers 971 # try failing headers
967 cl.env['HTTP_X-FORWARDED-HOST'] = 'whoami.net' 972 cl.env['HTTP_X-FORWARDED-HOST'] = 'whoami.net'
968 # this raises an error as the header check passes and 973 # this raises an error as the header check passes and
969 # it did the edit and tries to send mail. 974 # it did the edit and tries to send mail.
970 cl.inner_main() 975 cl.inner_main()
971 match_at=out[0].find('Invalid X-FORWARDED-HOST whoami.net') 976 match_at=out[0].find('Invalid X-FORWARDED-HOST whoami.net')
977 print "result of subtest 6:", out[0]
972 self.assertNotEqual(match_at, -1) 978 self.assertNotEqual(match_at, -1)
973 del(cl.env['HTTP_X-FORWARDED-HOST']) 979 del(cl.env['HTTP_X-FORWARDED-HOST'])
974 del(out[0]) 980 del(out[0])
975 981
976 # header checks succeed 982 # header checks succeed
977 # check nonce handling. 983 # check nonce handling.
978 cl.env['HTTP_REFERER'] = 'http://whoami.com/path/' 984 cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
985
986 # roundup will report a missing token.
987 cl.db.config['WEB_CSRF_ENFORCE_TOKEN'] = 'required'
988 cl.inner_main()
989 match_at=out[0].find('<p>Csrf token is missing.</p>')
990 print "result of subtest 6a:", out[0], match_at
991 self.assertEqual(match_at, 33)
992 del(out[0])
993 cl.db.config['WEB_CSRF_ENFORCE_TOKEN'] = 'yes'
979 994
980 import copy 995 import copy
981 form2 = copy.copy(form) 996 form2 = copy.copy(form)
982 form2.update({'@csrf': 'booogus'}) 997 form2.update({'@csrf': 'booogus'})
983 # add a bogus csrf field to the form and rerun the inner_main 998 # add a bogus csrf field to the form and rerun the inner_main
984 cl.form = makeForm(form2) 999 cl.form = makeForm(form2)
985 1000
986 cl.inner_main() 1001 cl.inner_main()
987 match_at=out[0].find('Invalid csrf token found: booogus') 1002 match_at=out[0].find('Invalid csrf token found: booogus')
1003 print "result of subtest 7:", out[0]
988 self.assertEqual(match_at, 36) 1004 self.assertEqual(match_at, 36)
989 del(out[0]) 1005 del(out[0])
990 1006
991 form2 = copy.copy(form) 1007 form2 = copy.copy(form)
992 nonce = anti_csrf_nonce(cl, cl) 1008 nonce = anti_csrf_nonce(cl, cl)
993 # verify that we can see the nonce 1009 # verify that we can see the nonce
994 otks = cl.db.getOTKManager() 1010 otks = cl.db.getOTKManager()
995 isitthere = otks.exists(nonce) 1011 isitthere = otks.exists(nonce)
1012 print "result of subtest 8:", isitthere
1013 print "otks: user, session", otks.get(nonce, 'uid', default=None), \
1014 otks.get(nonce, 'session', default=None)
996 self.assertEqual(isitthere, True) 1015 self.assertEqual(isitthere, True)
997 1016
998 form2.update({'@csrf': nonce}) 1017 form2.update({'@csrf': nonce})
999 # add a real csrf field to the form and rerun the inner_main 1018 # add a real csrf field to the form and rerun the inner_main
1000 cl.form = makeForm(form2) 1019 cl.form = makeForm(form2)
1001 cl.inner_main() 1020 cl.inner_main()
1002 # csrf passes and redirects to the new issue. 1021 # csrf passes and redirects to the new issue.
1003 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message') 1022 match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
1023 print "result of subtest 9:", out[0]
1004 self.assertEqual(match_at, 0) 1024 self.assertEqual(match_at, 0)
1005 del(out[0]) 1025 del(out[0])
1006 1026
1007 # try a replay attack 1027 # try a replay attack
1008 cl.inner_main() 1028 cl.inner_main()
1009 # This should fail as token was wiped by last run. 1029 # This should fail as token was wiped by last run.
1010 match_at=out[0].find('Invalid csrf token found: %s'%nonce) 1030 match_at=out[0].find('Invalid csrf token found: %s'%nonce)
1011 print "replay of csrf after post use", out[0] 1031 print "replay of csrf after post use", out[0]
1032 print "result of subtest 10:", out[0]
1012 self.assertEqual(match_at, 36) 1033 self.assertEqual(match_at, 36)
1013 del(out[0]) 1034 del(out[0])
1014 1035
1015 # make sure that a get deletes the csrf. 1036 # make sure that a get deletes the csrf.
1016 cl.env['REQUEST_METHOD'] = 'GET' 1037 cl.env['REQUEST_METHOD'] = 'GET'
1021 # add a real csrf field to the form and rerun the inner_main 1042 # add a real csrf field to the form and rerun the inner_main
1022 cl.form = makeForm(form2) 1043 cl.form = makeForm(form2)
1023 cl.inner_main() 1044 cl.inner_main()
1024 # csrf passes but fail creating new issue because not a post 1045 # csrf passes but fail creating new issue because not a post
1025 match_at=out[0].find('<p>Invalid request</p>') 1046 match_at=out[0].find('<p>Invalid request</p>')
1047 print "result of subtest 11:", out[0]
1026 self.assertEqual(match_at, 33) 1048 self.assertEqual(match_at, 33)
1027 del(out[0]) 1049 del(out[0])
1028 1050
1029 # the token should be gone 1051 # the token should be gone
1030 isitthere = otks.exists(nonce) 1052 isitthere = otks.exists(nonce)
1053 print "result of subtest 12:", isitthere
1031 self.assertEqual(isitthere, False) 1054 self.assertEqual(isitthere, False)
1032 1055
1033 # change to post and should fail w/ invalid csrf 1056 # change to post and should fail w/ invalid csrf
1034 # since get deleted the token. 1057 # since get deleted the token.
1035 cl.env.update({'REQUEST_METHOD': 'POST'}) 1058 cl.env.update({'REQUEST_METHOD': 'POST'})
1036 print cl.env 1059 print cl.env
1037 cl.inner_main() 1060 cl.inner_main()
1038 match_at=out[0].find('Invalid csrf token found: %s'%nonce) 1061 match_at=out[0].find('Invalid csrf token found: %s'%nonce)
1039 print "post failure after get", out[0] 1062 print "post failure after get", out[0]
1063 print "result of subtest 13:", out[0]
1040 self.assertEqual(match_at, 36) 1064 self.assertEqual(match_at, 36)
1041 del(out[0]) 1065 del(out[0])
1042 1066
1043 del(cl.env['HTTP_REFERER']) 1067 del(cl.env['HTTP_REFERER'])
1044 1068
1089 cl.handle_xmlrpc() 1113 cl.handle_xmlrpc()
1090 print out 1114 print out
1091 self.assertEqual(out[0], answer) 1115 self.assertEqual(out[0], answer)
1092 del(out[0]) 1116 del(out[0])
1093 1117
1094 # remove the X-REQUESTED-WITH header and get a failure. 1118 # remove the X-REQUESTED-WITH header and get an xmlrpc fault returned
1095 del(cl.env['HTTP_X-REQUESTED-WITH']) 1119 del(cl.env['HTTP_X-REQUESTED-WITH'])
1096 self.assertRaises(UsageError,cl.handle_xmlrpc) 1120 cl.handle_xmlrpc()
1121 output="<?xml version='1.0'?>\n<methodResponse>\n<fault>\n<value><struct>\n<member>\n<name>faultCode</name>\n<value><int>1</int></value>\n</member>\n<member>\n<name>faultString</name>\n<value><string>&lt;class 'roundup.exceptions.UsageError'&gt;:Required Header Missing</string></value>\n</member>\n</struct></value>\n</fault>\n</methodResponse>\n"
1122 print out[0]
1123 self.assertEqual(output,out[0])
1124 del(out[0])
1097 1125
1098 # change config to not require X-REQUESTED-WITH header 1126 # change config to not require X-REQUESTED-WITH header
1099 cl.db.config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] = 'logfailure' 1127 cl.db.config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] = 'logfailure'
1100 cl.handle_xmlrpc() 1128 cl.handle_xmlrpc()
1101 print out 1129 print out

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