4747from email .MIMEText import MIMEText
4848from email .MIMEMultipart import MIMEMultipart
4949import roundup .anypy .email_
50+ import xmlrpclib
5051
5152def initialiseSecurity (security ):
5253 '''Create some Permissions and Roles on the security object
@@ -436,15 +437,27 @@ def handle_xmlrpc(self):
436437 self .determine_user ()
437438 self .check_anonymous_access ()
438439
439- if self .handle_csrf (xmlrpc = True ):
440- # Call the appropriate XML-RPC method.
440+ try :
441+ # coverting from function returning true/false to
442+ # raising exceptions
443+ # Call csrf with xmlrpc checks enabled.
444+ # It will return True if everything is ok,
445+ # raises exception on check failure.
446+ csrf_ok = self .handle_csrf (xmlrpc = True )
447+ except (Unauthorised , UsageError ), msg :
448+ # report exception back to server
449+ exc_type , exc_value , exc_tb = sys .exc_info ()
450+ output = xmlrpclib .dumps (
451+ xmlrpclib .Fault (1 , "%s:%s" % (exc_type , exc_value )),
452+ allow_none = True )
453+ csrf_ok = False # we had an error, failed check
454+
455+ if csrf_ok == True :
441456 handler = xmlrpc .RoundupDispatcher (self .db ,
442457 self .instance .actions ,
443458 self .translator ,
444459 allow_none = True )
445460 output = handler .dispatch (input )
446- else :
447- raise Unauthorised , self ._error_message
448461
449462 self .setHeader ("Content-Type" , "text/xml" )
450463 self .setHeader ("Content-Length" , str (len (output )))
@@ -510,7 +523,17 @@ def inner_main(self):
510523 self .check_anonymous_access ()
511524
512525 # check for a valid csrf token identifying the right user
513- if self .handle_csrf ():
526+ csrf_ok = True
527+ try :
528+ # coverting from function returning true/false to
529+ # raising exceptions
530+ csrf_ok = self .handle_csrf ()
531+ except (UsageError , Unauthorised ), msg :
532+ csrf_ok = False
533+ self .form_wins = True
534+ self ._error_message = msg
535+
536+ if csrf_ok :
514537 # csrf checks pass. Run actions etc.
515538 # possibly handle a form submit action (may change
516539 # self.classname and self.template, and may also
@@ -943,7 +966,7 @@ def handle_csrf(self, xmlrpc=False):
943966 # can fix the nonce leakage and destroy it. (nonces
944967 # used in a get are more exposed than those used in a
945968 # post.) Note, I don't attempt to validate here since
946- # existance here is the sign of a failure. If nonce
969+ # existence here is the sign of a failure. If nonce
947970 # exists try to report the referer header to try to
948971 # find where this comes from so it can be fixed. If
949972 # nonce doesn't exist just ignore it. Maybe we should
@@ -960,15 +983,17 @@ def handle_csrf(self, xmlrpc=False):
960983 referer )
961984 otks .destroy (key )
962985 self .db .commit ()
986+ # do return here. Keys have been obsoleted.
987+ # we didn't do a expire cycle of session keys,
988+ # but that's ok.
963989 return True
964990
965991 config = self .instance .config
966- user = self .db .getuid ()
992+ current_user = self .db .getuid ()
967993
968- # the HTTP headers we check.
969- # Note that the xmlrpc header is missing. Its enforcement is
970- # different (yes/required are the same for example)
971- # so we don't include here.
994+ # List HTTP headers we check. Note that the xmlrpc header is
995+ # missing. Its enforcement is different (yes/required are the
996+ # same for example) so we don't include here.
972997 header_names = [
973998 "ORIGIN" ,
974999 "REFERER" ,
@@ -978,63 +1003,58 @@ def handle_csrf(self, xmlrpc=False):
9781003
9791004 header_pass = 0 # count of passing header checks
9801005
981- # if any headers are required and missing, report
982- # an error
1006+ # If required headers are missing, raise an error
9831007 for header in header_names :
9841008 if (config ["WEB_CSRF_ENFORCE_HEADER_%s" % header ] == 'required'
9851009 and "HTTP_%s" % header not in self .env ):
986- self .add_error_message (self ._ ("Missing header: %s" )% header )
987- logger .error (self ._ ("csrf header %s required but missing for user%s." ), header , user )
988- return False
1010+ logger .error (self ._ ("csrf header %s required but missing for user%s." ), header , current_user )
1011+ raise Unauthorised , self ._ ("Missing header: %s" )% header
9891012
990- # if you change these make sure to consider what
991- # happens if header variable exists but is empty.
992- # self.base.find("") returns 0 for example not -1
993- #
9941013 # self.base always matches: ^https?://hostname
995- enforce = config ['WEB_CSRF_ENFORCE_HEADER_ORIGIN' ]
996- if 'HTTP_ORIGIN' in self .env and enforce != "no" :
997- origin = self .env ['HTTP_ORIGIN' ]
998- foundat = self .base .find (origin + '/' )
999- if foundat != 0 :
1000- if enforce in ('required' , 'yes' ):
1001- self .add_error_message ("Invalid Origin %s" % origin )
1002- logger .error (self ._ ("csrf Origin header check failed for user%s. Value=%s" ), user , origin )
1003- return False
1004- elif enforce == 'logfailure' :
1005- logger .warning (self ._ ("csrf Origin header check failed for user%s. Value=%s" ), user , origin )
1006- else :
1007- header_pass += 1
1008-
10091014 enforce = config ['WEB_CSRF_ENFORCE_HEADER_REFERER' ]
10101015 if 'HTTP_REFERER' in self .env and enforce != "no" :
10111016 referer = self .env ['HTTP_REFERER' ]
10121017 # self.base always has trailing /
10131018 foundat = referer .find (self .base )
10141019 if foundat != 0 :
10151020 if enforce in ('required' , 'yes' ):
1016- self .add_error_message (self ._ ("Invalid Referer %s, %s" )% (referer ,self .base ))
1017- logger .error (self ._ ("csrf Referer header check failed for user%s. Value=%s" ), user , referer )
1018- return False
1021+ logger .error (self ._ ("csrf Referer header check failed for user%s. Value=%s" ), current_user , referer )
1022+ raise Unauthorised , self ._ ("Invalid Referer %s, %s" )% (referer ,self .base )
10191023 elif enforce == 'logfailure' :
1020- logger .warning (self ._ ("csrf Referer header check failed for user%s. Value=%s" ), user , referer )
1024+ logger .warning (self ._ ("csrf Referer header check failed for user%s. Value=%s" ), current_user , referer )
10211025 else :
10221026 header_pass += 1
10231027
1024- enforce = config ['WEB_CSRF_ENFORCE_HEADER_X-FORWARDED-HOST' ]
1025- if 'HTTP_X-FORWARDED-HOST' in self .env and enforce != "no" :
1026- host = self .env ['HTTP_X-FORWARDED-HOST' ]
1027- foundat = self .base .find ('://' + host + '/' )
1028- # 4 means self.base has http:/ prefix, 5 means https:/ prefix
1029- if foundat not in [4 , 5 ]:
1028+ # if you change these make sure to consider what
1029+ # happens if header variable exists but is empty.
1030+ # self.base.find("") returns 0 for example not -1
1031+ enforce = config ['WEB_CSRF_ENFORCE_HEADER_ORIGIN' ]
1032+ if 'HTTP_ORIGIN' in self .env and enforce != "no" :
1033+ origin = self .env ['HTTP_ORIGIN' ]
1034+ foundat = self .base .find (origin + '/' )
1035+ if foundat != 0 :
10301036 if enforce in ('required' , 'yes' ):
1031- self .add_error_message (self ._ ("Invalid X-FORWARDED-HOST %s" )% host )
1032- logger .error (self ._ ("csrf X-FORWARDED-HOST header check failed for user%s. Value=%s" ), user , host )
1033- return False
1037+ logger .error (self ._ ("csrf Origin header check failed for user%s. Value=%s" ), current_user , origin )
1038+ raise Unauthorised , self ._ ("Invalid Origin %s" % origin )
10341039 elif enforce == 'logfailure' :
1035- logger .warning (self ._ ("csrf X-FORWARDED-HOST header check failed for user%s. Value=%s" ), user , host )
1040+ logger .warning (self ._ ("csrf Origin header check failed for user%s. Value=%s" ), current_user , origin )
10361041 else :
10371042 header_pass += 1
1043+
1044+ enforce = config ['WEB_CSRF_ENFORCE_HEADER_X-FORWARDED-HOST' ]
1045+ if 'HTTP_X-FORWARDED-HOST' in self .env :
1046+ if enforce != "no" :
1047+ host = self .env ['HTTP_X-FORWARDED-HOST' ]
1048+ foundat = self .base .find ('://' + host + '/' )
1049+ # 4 means self.base has http:/ prefix, 5 means https:/ prefix
1050+ if foundat not in [4 , 5 ]:
1051+ if enforce in ('required' , 'yes' ):
1052+ logger .error (self ._ ("csrf X-FORWARDED-HOST header check failed for user%s. Value=%s" ), current_user , host )
1053+ raise Unauthorised , self ._ ("Invalid X-FORWARDED-HOST %s" )% host
1054+ elif enforce == 'logfailure' :
1055+ logger .warning (self ._ ("csrf X-FORWARDED-HOST header check failed for user%s. Value=%s" ), current_user , host )
1056+ else :
1057+ header_pass += 1
10381058 else :
10391059 # https://seclab.stanford.edu/websec/csrf/csrf.pdf
10401060 # recommends checking HTTP HOST header as well.
@@ -1049,19 +1069,17 @@ def handle_csrf(self, xmlrpc=False):
10491069 # 4 means http:// prefix, 5 means https:// prefix
10501070 if foundat not in [4 , 5 ]:
10511071 if enforce in ('required' , 'yes' ):
1052- self .add_error_message (self ._ ("Invalid HOST %s" )% host )
1053- logger .error (self ._ ("csrf HOST header check failed for user%s. Value=%s" ), user , host )
1054- return False
1072+ logger .error (self ._ ("csrf HOST header check failed for user%s. Value=%s" ), current_user , host )
1073+ raise Unauthorised , self ._ ("Invalid HOST %s" )% host
10551074 elif enforce == 'logfailure' :
1056- logger .warning (self ._ ("csrf HOST header check failed for user%s. Value=%s" ), user , host )
1075+ logger .warning (self ._ ("csrf HOST header check failed for user%s. Value=%s" ), current_user , host )
10571076 else :
10581077 header_pass += 1
10591078
10601079 enforce = config ['WEB_CSRF_HEADER_MIN_COUNT' ]
10611080 if header_pass < enforce :
1062- self .add_error_message (self ._ ("Unable to verify sufficient headers" ))
10631081 logger .error (self ._ ("Csrf: unable to verify sufficient headers" ))
1064- return False
1082+ raise UsageError , self . _ ( "Unable to verify sufficient headers" )
10651083
10661084 enforce = config ['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH' ]
10671085 if xmlrpc :
@@ -1074,25 +1092,8 @@ def handle_csrf(self, xmlrpc=False):
10741092 #
10751093 # see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers
10761094 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
1084-
1085- enforce = config ['WEB_CSRF_ENFORCE_TOKEN' ]
1086- if "@csrf" not in self .form :
1087- if enforce == 'required' :
1088- self .add_error_message (self ._ ("No csrf token found" ))
1089- logger .error (self ._ ("required csrf field missing for user%s" ), user )
1090- return False
1091- else :
1092- if enforce == 'logfailure' :
1093- # FIXME include url
1094- logger .warning (self ._ ("required csrf field missing for user%s" ), user )
1095- return True
1095+ logger .error (self ._ ("csrf X-REQUESTED-WITH xmlrpc required header check failed for user%s." ), current_user )
1096+ raise UsageError , self ._ ("Required Header Missing" )
10961097
10971098 # Expire old csrf tokens now so we don't use them. These will
10981099 # be committed after the otks.destroy below. Note that the
@@ -1102,15 +1103,43 @@ def handle_csrf(self, xmlrpc=False):
11021103 # our own.
11031104 otks .clean ()
11041105
1105- key = self .form ['@csrf' ].value
1106- uid = otks .get (key , 'uid' , default = None )
1107- sid = otks .get (key , 'sid' , default = None )
1106+ if xmlrpc :
1107+ # Save removal of expired keys from database.
1108+ self .db .commit ()
1109+ # Return from here since we have done housekeeping
1110+ # and don't use csrf tokens for xmlrpc.
1111+ return True
1112+
1113+ # process @csrf tokens past this point.
1114+ key = None
1115+ nonce_user = None
1116+ nonce_session = None
11081117
1109- # The key has been used or compromised.
1110- # Delete it to prevent replay.
1111- otks .destroy (key )
1118+ if '@csrf' in self .form :
1119+ key = self .form ['@csrf' ].value
1120+
1121+ nonce_user = otks .get (key , 'uid' , default = None )
1122+ nonce_session = otks .get (key , 'sid' , default = None )
1123+ # The key has been used or compromised.
1124+ # Delete it to prevent replay.
1125+ otks .destroy (key )
1126+
1127+ # commit the deletion/expiration of all keys
11121128 self .db .commit ()
11131129
1130+ enforce = config ['WEB_CSRF_ENFORCE_TOKEN' ]
1131+ if key is None : # we do not have an @csrf token
1132+ if enforce == 'required' :
1133+ logger .error (self ._ ("Required csrf field missing for user%s" ), current_user )
1134+ raise UsageError , self ._ ("Csrf token is missing." )
1135+ elif enforce == 'logfailure' :
1136+ # FIXME include url
1137+ logger .warning (self ._ ("csrf field not supplied by user%s" ), current_user )
1138+ else :
1139+ # enforce is either yes or no. Both permit change if token is
1140+ # missing
1141+ return True
1142+
11141143 current_session = self .session_api ._sid
11151144
11161145 '''
@@ -1127,18 +1156,18 @@ def handle_csrf(self, xmlrpc=False):
11271156 # The csrf token for the login button is associated
11281157 # with the prior login, so it will not validate.
11291158 #
1130- # To bypass error, Verify that uid != user and that
1159+ # To bypass error, Verify that nonce_user != user and that
11311160 # user is '2' (anonymous) and there is no current
11321161 # session key. Validate that the csrf exists
1133- # in the db and uid and sid are not None.
1162+ # in the db and nonce_user and nonce_session are not None.
11341163 # Also validate that the action is Login.
11351164 # Lastly requre at least one csrf header check to pass.
11361165 # If all of those work process the login.
1137- if user != uid and \
1138- user == '2' and \
1166+ if current_user != nonce_user and \
1167+ current_user == '2' and \
11391168 current_session is None and \
1140- uid is not None and \
1141- sid is not None and \
1169+ nonce_user is not None and \
1170+ nonce_session is not None and \
11421171 "@action" in self.form and \
11431172 self.form["@action"].value == "Login":
11441173 if header_pass > 0:
@@ -1148,23 +1177,28 @@ def handle_csrf(self, xmlrpc=False):
11481177 else:
11491178 self.add_error_message("Reload window before logging in.")
11501179 '''
1151-
11521180 # validate against user and session
1153- if uid != user :
1181+ if current_user != nonce_user :
11541182 if enforce in ('required' , "yes" ):
1155- self .add_error_message (self ._ ("Invalid csrf token found: %s" )% key )
1156- logger .error (self ._ ("csrf uid mismatch for user%s." ), user )
1157- return False
1183+ logger .error (
1184+ self ._ ("Csrf mismatch user: current user %s != stored user %s, current session, stored session: %s,%s for key %s." ),
1185+ current_user , nonce_user , current_session , nonce_session , key )
1186+ raise UsageError , self ._ ("Invalid csrf token found: %s" )% key
11581187 elif enforce == 'logfailure' :
1159- logger .warning (self ._ ("csrf uid mismatch for user%s." ), user )
1160- if current_session != sid :
1188+ logger .warning (
1189+ self ._ ("logged only: Csrf mismatch user: current user %s != stored user %s, current session, stored session: %s,%s for key %s." ),
1190+ current_user , nonce_user , current_session , nonce_session , key )
1191+ if current_session != nonce_session :
11611192 if enforce in ('required' , "yes" ):
1162- self . add_error_message ( self . _ (
1163- "Invalid csrf session found : %s" ) % key )
1164- logger . error ( self . _ ( "csrf session mismatch for user%s." ), user )
1165- return False
1193+ logger . error (
1194+ self . _ ( "Csrf mismatch user: current session %s != stored session %s, current user/stored user is : %s for key %s." ),
1195+ current_session , nonce_session , current_user , key )
1196+ raise UsageError , self . _ ( "Invalid csrf session found: %s" ) % key
11661197 elif enforce == 'logfailure' :
1167- logger .warning (self ._ ("csrf session mismatch for user%s." ), user )
1198+ logger .warning (
1199+ self ._ ("logged only: Csrf mismatch user: current session %s != stored session %s, current user/stored user is: %s for key %s." ),
1200+ current_session , nonce_session , current_user , key )
1201+ # we are done and the change can occur.
11681202 return True
11691203
11701204 def opendb (self , username ):
0 commit comments