Skip to content

Commit 91e527f

Browse files
committed
Added tests for csrf with xmlrpc.
Fixed the code for xmlrpc csrf defense: raise UsageError if X-REQUESTED-WITH header is required and missing. if HTTP_AUTHORIZATION is used, properly seed the random number generator using the password.
1 parent 5f9c23b commit 91e527f

File tree

2 files changed

+78
-12
lines changed

2 files changed

+78
-12
lines changed

roundup/cgi/client.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
from roundup import roundupdb, date, hyperdb, password
2929
from roundup.cgi import templating, cgitb, TranslationService
3030
from roundup.cgi import actions
31-
from roundup.exceptions import LoginError, Reject, RejectRaw, Unauthorised
31+
from roundup.exceptions import LoginError, Reject, RejectRaw, \
32+
Unauthorised, UsageError
3233
from roundup.cgi.exceptions import (
3334
FormError, NotFound, NotModified, Redirect, SendFile, SendStaticFile,
3435
DetectorError, SeriousError)
@@ -817,7 +818,7 @@ def determine_user(self):
817818
# try to seed with something harder to guess than
818819
# just the time. If random is SystemRandom,
819820
# this is a no-op.
820-
random.seed(password + time.time())
821+
random.seed("%s%s"%(password,time.time()))
821822

822823
# if user was not set by http authorization, try session lookup
823824
if not user:
@@ -1063,16 +1064,23 @@ def handle_csrf(self, xmlrpc=False):
10631064
return False
10641065

10651066
enforce=config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH']
1066-
if xmlrpc and (enforce in ['required', 'yes']):
1067-
# if we get here we have usually passed at least one
1068-
# header check. We check for presence of this custom
1069-
# header for xmlrpc calls only.
1070-
# E.G. X-Requested-With: XMLHttpRequest
1071-
# see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers
1072-
if 'HTTP_X-REQUESTED-WITH' not in self.env:
1073-
self.add_error_message("Required Header Missing")
1074-
logger.error(self._("csrf X-REQUESED-WITH xmlrpc required header check failed for user%s."), user)
1075-
return False
1067+
if xmlrpc:
1068+
if enforce in ['required', 'yes']:
1069+
# if we get here we have usually passed at least one
1070+
# header check. We check for presence of this custom
1071+
# header for xmlrpc calls only.
1072+
# E.G. X-Requested-With: XMLHttpRequest
1073+
# Note we do not use CSRF nonces for xmlrpc requests.
1074+
#
1075+
# see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers
1076+
if 'HTTP_X-REQUESTED-WITH' not in self.env:
1077+
logger.error(self._("csrf X-REQUESTED-WITH xmlrpc required header check failed for user%s."), user)
1078+
raise UsageError, "Required Header Missing"
1079+
return False # unreached
1080+
else:
1081+
return True
1082+
else:
1083+
return True
10761084

10771085
enforce=config['WEB_CSRF_ENFORCE_TOKEN']
10781086
if "@csrf" not in self.form:

test/test_cgi.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from roundup.cgi import client, actions, exceptions
1414
from roundup.cgi.exceptions import FormError
15+
from roundup.exceptions import UsageError
1516
from roundup.cgi.templating import HTMLItem, HTMLRequest, NoTemplate, anti_csrf_nonce
1617
from roundup.cgi.templating import HTMLProperty, _HTMLItem
1718
from roundup.cgi.form_parser import FormParser
@@ -1046,6 +1047,61 @@ def hasPermission(s, p, classname=None, d=None, e=None, **kw):
10461047
os.remove(SENDMAILDEBUG)
10471048
#raise ValueError
10481049

1050+
def testXmlrpcCsrfProtection(self):
1051+
# set the password for admin so we can log in.
1052+
passwd=password.Password('admin')
1053+
self.db.user.set('1', password=passwd)
1054+
1055+
out = []
1056+
def wh(s):
1057+
out.append(s)
1058+
1059+
# xmlrpc has no form content
1060+
form = {}
1061+
cl = client.Client(self.instance, None,
1062+
{'REQUEST_METHOD':'POST',
1063+
'PATH_INFO':'xmlrpc',
1064+
'CONTENT_TYPE': 'text/plain',
1065+
'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=',
1066+
'HTTP_REFERER': 'http://whoami.com/path/',
1067+
'HTTP_X-REQUESTED-WITH': "XMLHttpRequest"
1068+
}, form)
1069+
cl.db = self.db
1070+
cl.base = 'http://whoami.com/path/'
1071+
cl._socket_op = lambda *x : True
1072+
cl._error_message = []
1073+
cl.request = MockNull()
1074+
cl.write = wh # capture output
1075+
1076+
# Should return explanation because content type is text/plain
1077+
# and not text/xml
1078+
cl.handle_xmlrpc()
1079+
self.assertEqual(out[0], "This is the endpoint of Roundup <a href='http://www.roundup-tracker.org/docs/xmlrpc.html'>XML-RPC interface</a>.")
1080+
del(out[0])
1081+
1082+
# Should return admin user indicating auth works and
1083+
# header checks succeed (REFERER and X-REQUESTED-WITH)
1084+
cl.env['CONTENT_TYPE'] = "text/xml"
1085+
# ship the form with the value holding the xml value.
1086+
# I have no clue why this works but ....
1087+
cl.form = MockNull(file = True, value = "<?xml version='1.0'?>\n<methodCall>\n<methodName>display</methodName>\n<params>\n<param>\n<value><string>user1</string></value>\n</param>\n<param>\n<value><string>username</string></value>\n</param>\n</params>\n</methodCall>\n" )
1088+
answer ="<?xml version='1.0'?>\n<methodResponse>\n<params>\n<param>\n<value><struct>\n<member>\n<name>username</name>\n<value><string>admin</string></value>\n</member>\n</struct></value>\n</param>\n</params>\n</methodResponse>\n"
1089+
cl.handle_xmlrpc()
1090+
print out
1091+
self.assertEqual(out[0], answer)
1092+
del(out[0])
1093+
1094+
# remove the X-REQUESTED-WITH header and get a failure.
1095+
del(cl.env['HTTP_X-REQUESTED-WITH'])
1096+
self.assertRaises(UsageError,cl.handle_xmlrpc)
1097+
1098+
# change config to not require X-REQUESTED-WITH header
1099+
cl.db.config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] = 'logfailure'
1100+
cl.handle_xmlrpc()
1101+
print out
1102+
self.assertEqual(out[0], answer)
1103+
del(out[0])
1104+
10491105
#
10501106
# SECURITY
10511107
#
@@ -1481,6 +1537,7 @@ def testRenderAltTemplates(self):
14811537

14821538
result = self.client.renderContext()
14831539
print result
1540+
# sha1sum of classic tracker user.forgotten.template must be found
14841541
self.assertNotEqual(-1,
14851542
result.index('<!-- SHA: eb5dd0bec7a57d58cb7edbeb939fb0390ed1bf74 -->'))
14861543

@@ -1495,6 +1552,7 @@ def testRenderAltTemplates(self):
14951552

14961553
result = self.client.renderContext()
14971554
print result
1555+
# sha1sum of classic tracker user.item.template must be found
14981556
self.assertNotEqual(-1,
14991557
result.index('<!-- SHA: 3b7ce7cbf24f77733c9b9f64a569d6429390cc3f -->'))
15001558

0 commit comments

Comments
 (0)