Skip to content

Commit 6a88660

Browse files
committed
Remove csrf keys used with get
The original code didn't delete keys used with a get. Detect this and invalidate the keys. Get keys are more likely to leak (in urls etc.) so they have to be removed once used. Also a little code cleanup and added testing.
1 parent b267c1b commit 6a88660

File tree

2 files changed

+86
-9
lines changed

2 files changed

+86
-9
lines changed

roundup/cgi/client.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -929,9 +929,38 @@ def handle_csrf(self, xmlrpc=False):
929929
debugging seems to be an advantage.
930930
931931
'''
932+
# Create the otks handle here as we need it almost immediately.
933+
# If this is perf issue, set to None here and check below
934+
# once all header checks have passed if it needs to be opened.
935+
otks=self.db.getOTKManager()
936+
932937
# Assume: never allow changes via GET
933938
if self.env['REQUEST_METHOD'] not in ['POST', 'PUT', 'DELETE']:
939+
if "@csrf" in self.form:
940+
# We have a nonce being used with a method it should
941+
# not be. If the nonce exists, report to admin so they
942+
# can fix the nonce leakage and destroy it. (nonces
943+
# used in a get are more exposed than those used in a
944+
# post.) Note, I don't attempt to validate here since
945+
# existance here is the sign of a failure. If nonce
946+
# exists try to report the referer header to try to
947+
# find where this comes from so it can be fixed. If
948+
# nonce doesn't exist just ignore it. Maybe we should
949+
# report, but somebody could spam us with a ton of
950+
# invalid keys and fill up the logs.
951+
if 'HTTP_REFERER' in self.env:
952+
referer = self.env['HTTP_REFERER']
953+
else:
954+
referer = self._("Referer header not available.")
955+
key=self.form['@csrf'].value
956+
if otks.exists(key):
957+
logger.error(
958+
self._("csrf key used with wrong method from: %s"),
959+
referer)
960+
otks.destroy(key)
961+
self.db.commit()
934962
return True
963+
935964
config=self.instance.config
936965
user=self.db.getuid()
937966

@@ -1045,7 +1074,6 @@ def handle_csrf(self, xmlrpc=False):
10451074
logger.error(self._("csrf X-REQUESED-WITH xmlrpc required header check failed for user%s."), user)
10461075
return False
10471076

1048-
otks=self.db.getOTKManager()
10491077
enforce=config['WEB_CSRF_ENFORCE_TOKEN']
10501078
if "@csrf" not in self.form:
10511079
if enforce == 'required':
@@ -1061,8 +1089,14 @@ def handle_csrf(self, xmlrpc=False):
10611089
key=self.form['@csrf'].value
10621090
uid = otks.get(key, 'uid', default=None)
10631091
sid = otks.get(key, 'sid', default=None)
1092+
if __debug__:
1093+
ts = otks.get(key, '__timestamp', default=None)
1094+
print("Found key %s for user%s sess: %s, ts %s, time %s"%(key, uid, sid, ts, time.time()))
10641095
current_session = self.session_api._sid
1065-
# validate against user and session
1096+
1097+
# The key has been used or compromised. Delete it to prevent replay.
1098+
otks.destroy(key)
1099+
self.db.commit()
10661100

10671101
'''
10681102
# I think now that LogoutAction redirects to
@@ -1100,20 +1134,19 @@ def handle_csrf(self, xmlrpc=False):
11001134
self.add_error_message("Reload window before logging in.")
11011135
'''
11021136

1103-
# The key has been used or compromised. Delete it to prevent replay.
1104-
otks.destroy(key)
1105-
self.db.commit()
1106-
1137+
# validate against user and session
11071138
if uid != user:
11081139
if enforce in ('required', "yes"):
11091140
self.add_error_message(self._("Invalid csrf token found: %s")%key)
1141+
logger.error(self._("csrf uid mismatch for user%s."), user)
11101142
return False
11111143
elif enforce == 'logfailure':
11121144
logger.warning(self._("csrf uid mismatch for user%s."), user)
11131145
if current_session != sid:
11141146
if enforce in ('required', "yes"):
11151147
self.add_error_message(self._(
11161148
"Invalid csrf session found: %s")%key)
1149+
logger.error(self._("csrf session mismatch for user%s."), user)
11171150
return False
11181151
elif enforce == 'logfailure':
11191152
logger.warning(self._("csrf session mismatch for user%s."), user)

test/test_cgi.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -972,31 +972,75 @@ def hasPermission(s, p, classname=None, d=None, e=None, **kw):
972972
del(cl.env['HTTP_X-FORWARDED-HOST'])
973973
del(out[0])
974974

975-
import copy
975+
# header checks succeed
976+
# check nonce handling.
977+
cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
976978

979+
import copy
977980
form2 = copy.copy(form)
978981
form2.update({'@csrf': 'booogus'})
979982
# add a bogus csrf field to the form and rerun the inner_main
980983
cl.form = makeForm(form2)
981984

982-
cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
983985
cl.inner_main()
984986
match_at=out[0].find('Invalid csrf token found: booogus')
985987
self.assertEqual(match_at, 36)
986988
del(out[0])
987989

988990
form2 = copy.copy(form)
989991
nonce = anti_csrf_nonce(cl, cl)
992+
# verify that we can see the nonce
993+
otks = cl.db.getOTKManager()
994+
isitthere = otks.exists(nonce)
995+
self.assertEqual(isitthere, True)
996+
990997
form2.update({'@csrf': nonce})
991998
# add a real csrf field to the form and rerun the inner_main
992999
cl.form = makeForm(form2)
9931000
cl.inner_main()
9941001
# csrf passes and redirects to the new issue.
9951002
match_at=out[0].find('Redirecting to <a href="http://whoami.com/path/issue1?@ok_message')
9961003
self.assertEqual(match_at, 0)
997-
del(cl.env['HTTP_REFERER'])
9981004
del(out[0])
9991005

1006+
# try a replay attack
1007+
cl.inner_main()
1008+
# This should fail as token was wiped by last run.
1009+
match_at=out[0].find('Invalid csrf token found: %s'%nonce)
1010+
print "replay of csrf after post use", out[0]
1011+
self.assertEqual(match_at, 36)
1012+
del(out[0])
1013+
1014+
# make sure that a get deletes the csrf.
1015+
cl.env['REQUEST_METHOD'] = 'GET'
1016+
cl.env['HTTP_REFERER'] = 'http://whoami.com/path/'
1017+
form2 = copy.copy(form)
1018+
nonce = anti_csrf_nonce(cl, cl)
1019+
form2.update({'@csrf': nonce})
1020+
# add a real csrf field to the form and rerun the inner_main
1021+
cl.form = makeForm(form2)
1022+
cl.inner_main()
1023+
# csrf passes but fail creating new issue because not a post
1024+
match_at=out[0].find('<p>Invalid request</p>')
1025+
self.assertEqual(match_at, 33)
1026+
del(out[0])
1027+
1028+
# the token should be gone
1029+
isitthere = otks.exists(nonce)
1030+
self.assertEqual(isitthere, False)
1031+
1032+
# change to post and should fail w/ invalid csrf
1033+
# since get deleted the token.
1034+
cl.env.update({'REQUEST_METHOD': 'POST'})
1035+
print cl.env
1036+
cl.inner_main()
1037+
match_at=out[0].find('Invalid csrf token found: %s'%nonce)
1038+
print "post failure after get", out[0]
1039+
self.assertEqual(match_at, 36)
1040+
del(out[0])
1041+
1042+
del(cl.env['HTTP_REFERER'])
1043+
10001044
# clean up from email log
10011045
if os.path.exists(SENDMAILDEBUG):
10021046
os.remove(SENDMAILDEBUG)

0 commit comments

Comments
 (0)