Mercurial > p > roundup > code
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><class 'roundup.exceptions.UsageError'>: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 |
