Case Study : Exploiting a Business Logic Flaw with GitHub’s Forgot Password workflow (discovered by John Gracey)

Original text by Chetan Conikee

John Gracey of Wisdom published a very interesting business logic flaw in GitHub’s reset password workflow on November 28th, 2019. It was acknowledged and fixed by GitHub’s security team. If not mitigated, this flaw can lead to account takeover vulnerability (specifically for accounts with 2FA not enabled).

From ASCII to Unicode

ASCII (American Standard Code for Information Interchange) had became the first widespread encoding scheme. However, it was limited to only 128 character definitions. This was fine for the most common English characters, numbers, and punctuation, but slowly became limiting for the rest of the world.

Naturally, the rest of the world wanted the same encoding scheme for their characters too, which was why the Unicode standard was created. The objective of Unicode was to unify all the different encoding schemes so that the confusion between computers can be limited as much as possible.

As John Gracey points out, developer understanding of unicode is often limited to internationalization and hence fail to grok details associated with unicode points and units. This lack of understanding could lead to an inherent vulnerability called Unicode Case Mapping Collision.

Loosely speaking, a collision occurs when two different characters are uppercased or lowercased into the same character. This effect is found commonly at the boundary between two different protocols, like email and domain names.

~ John Gracey

On November 24th 2019, GetWisdom had published an exhaustive list of case mapping collisions with english alphabets here . Following this article, John published a detailed case study of the logic flaw here. I’d recommend for you all to read John’s post in detail before you proceed further.

Hacking Unicode Case Mapping Collision

Let us attempt to emulate this business logic workflow associated with resetPassword functionality

  1. Attacker enumerates with a unicode character embedded in local part of email address (not domain part). For example:`jıll@service.com`
  2. Attacker clicks forgot-password and types the email (for example: `jıll@service.com` where `ı` is the unicode character)
  3. The business logic supporting forgot-password function receives the attacker controlled email address and case-folds (toLowerCase) as a part of sanitization practice. This case folding transformation leads to a Unicode Case Mapping Collision which fundamentally transforms the identity to another user’s email address — `jıll@service.com` with a unicode `ı` is transformed into `jill@service.com` due to case mapping collision.
  4. Of course, the validation passes leading to next step of creating a reset link and dispatching an email to address specified via request (which is attacker controlled) and NOT to email-address associated with registered account (retrieved after validating identity).

Let us use this sample spring-boot based application (forked and revised) with forgot password functionality that emulates both a best and worse scenario associated with this logic flaw.conikeec/spring-security-registrationIf you’re already a student of Learn Spring Security, you can get started diving deeper into registration with Module 2…github.com

Refer to controller logic supporting password reset here (with all symptoms that can lead to an exploit)

  1. Attacker enumerates Forgot Password function in SaaS service with an embedded unicode character.
  2. Attacker controlled userEmail parameter is injected into the resetPasswordBad controller routine.
  3. Validation function findUserByEmailaccepts attacker controlled email address that is transformed (via caseFolding) and passes validation condition (if registered user exists).
  4. Email with reset password link is now sent to to address specified via request (which is attacker controlled) and NOT to email-address associated with registered account (retrieved after validating identity).

Automated verification of Business Logic flaws in source code

Let’s fire up ShiftLeft’s Ocular query engine and trace through information flows in order identify all of these missteps leading to this Business Logic Flaw.

git clone git@github.com:conikeec/spring-security-registration.git

cd spring-security-registration

//compile and create package artifact
mvn -Dmaven.test.skip=true clean package

// Download trial distribution of Ocular (https://ocular.shiftleft.io). Install and thereafter fire up the prompt to commence investigation

./ocular.sh

createCpgAndSp("/Users/chetanconikee/pgithub/spring-security-registration/target/spring-security-login-and-registration.war")


//retrieve controller mapped to resetPassword route
case class RouteMapping(routeName : String, backingController : String)
val attackSurface = cpg.annotation.name("RequestMapping").map(x =>
    RouteMapping(x.start.parameterAssign.value.code.l.head, x.start.method.fullName.l.head)
).l

//output
attackSurface: List[RouteMapping] = List(
  RouteMapping(
    "[\"/user/updatePassword\"]",
    "org.baeldung.web.controller.RegistrationController.changeUserPassword:org.baeldung.web.util.GenericResponse(java.util.Locale,org.baeldung.web.dto.PasswordDto)"
  ),
  RouteMapping(
    "[\"/user/changePassword\"]",
    "org.baeldung.web.controller.RegistrationController.showChangePasswordPage:java.lang.String(java.util.Locale,org.springframework.ui.Model,long,java.lang.String)"
  ),
  RouteMapping(
    "[\"/registrationConfirm\"]",
    "org.baeldung.web.controller.RegistrationController.confirmRegistration:java.lang.String(javax.servlet.http.HttpServletRequest,org.springframework.ui.Model,java.lang.String)"
  ),
  RouteMapping(
    "[\"/loggedUsersFromSessionRegistry\"]",
    "org.baeldung.web.controller.UserController.getLoggedUsersFromSessionRegistry:java.lang.String(java.util.Locale,org.springframework.ui.Model)"
  ),
  RouteMapping(
    "[\"/user/resendRegistrationToken\"]",
    "org.baeldung.web.controller.RegistrationController.resendRegistrationToken:org.baeldung.web.util.GenericResponse(javax.servlet.http.HttpServletRequest,java.lang.String)"
  ),
  RouteMapping(
    "[\"/loggedUsers\"]",
    "org.baeldung.web.controller.UserController.getLoggedUsers:java.lang.String(java.util.Locale,org.springframework.ui.Model)"
  ),
  RouteMapping(
    "[\"/user/resetPassword\"]",
    "org.baeldung.web.controller.RegistrationController.resetPassword:org.baeldung.web.util.GenericResponse(javax.servlet.http.HttpServletRequest,java.lang.String)"
  ),
  RouteMapping(
    "[\"/user/registrationCaptcha\"]",
    "org.baeldung.web.controller.RegistrationCaptchaController.captchaRegisterUserAccount:org.baeldung.web.util.GenericResponse(org.baeldung.web.dto.UserDto,javax.servlet.http.HttpServletRequest)"
  ),
  RouteMapping(
    "[\"/user/savePassword\"]",
    "org.baeldung.web.controller.RegistrationController.savePassword:org.baeldung.web.util.GenericResponse(java.util.Locale,org.baeldung.web.dto.PasswordDto)"
  ),
  RouteMapping(
    "[\"/user/registration\"]",
    "org.baeldung.web.controller.RegistrationController.registerUserAccount:org.baeldung.web.util.GenericResponse(org.baeldung.web.dto.UserDto,javax.servlet.http.HttpServletRequest)"
  ),
  RouteMapping(
    "[\"/user/update/2fa\"]",
    "org.baeldung.web.controller.RegistrationController.modifyUser2FA:org.baeldung.web.util.GenericResponse(boolean)"
  ),
  RouteMapping(
    "[\"/user/resetPasswordBad\"]",
    "org.baeldung.web.controller.RegistrationController.resetPasswordBad:org.baeldung.web.util.GenericResponse(javax.servlet.http.HttpServletRequest,java.lang.String)"
  )
)

At this stage we have extracted the attack surface and identified all controller functions mapped to exposed routes. Let us proceed to next step.

This route particularly is of interest to us is

RouteMapping( “[\”/user/resetPasswordBad\”]”, “org.baeldung.web.controller.RegistrationController.resetPasswordBad:org.baeldung.web.util.GenericResponse(javax.servlet.http.HttpServletRequest,java.lang.String)” )

CONDITION #1 : Attacker controlled vector (email) with unicode in local part is case folded and then passed to database validation routine

//define the source function and attacker controlled vector (which is the email address parameter)
val source = cpg.method.fullNameExact("org.baeldung.web.controller.RegistrationController.resetPasswordBad:org.baeldung.web.util.GenericResponse(javax.servlet.http.HttpServletRequest,java.lang.String)").parameter.evalType("java.lang.String")

// The DB lookup function is a part of the IUserService interface, implemented by UserService here https://github.com/conikeec/spring-security-registration/blob/master/src/main/java/org/baeldung/service/UserService.java#L136
val DB_LOOKUP_FN_EXPR = ".*findUserByEmail.*"

//define the sink function that participates in the data flow
val sink = cpg.method.name(DB_LOOKUP_FN_EXPR).parameter.evalType("java.lang.String")

// Verify BUSINESS LOGIC FLAW check to determine if attack controller vector (email) is caseFolded prior to DB lookup
sink.reachableBy(source).flows.passes(_.isCall.name(".*toLowerCase.*")).p

  """ _____________________________________________________________________________________________________________________
 | tracked                | lineNumber| method               | file                                                   |
 |====================================================================================================================|
 | userEmail              | 134       | resetPasswordBad     | org/baeldung/web/controller/RegistrationController.java|
 | userEmail              | 135       | resetPasswordBad     | org/baeldung/web/controller/RegistrationController.java|
 | this                   | N/A       | toLowerCase          | java/lang/String.java                                  |
 | ret                    | N/A       | toLowerCase          | java/lang/String.java                                  |
 | userEmail.toLowerCase()| 135       | resetPasswordBad     | org/baeldung/web/controller/RegistrationController.java|
 | param1                 | N/A       | .assignment| N/A                                                    |
 | param0                 | N/A       | .assignment| N/A                                                    |
 | $r1                    | 135       | resetPasswordBad     | org/baeldung/web/controller/RegistrationController.java|
 | $r1                    | 135       | resetPasswordBad     | org/baeldung/web/controller/RegistrationController.java|
 | param0                 | N/A       | findUserByEmail      | org/baeldung/service/IUserService.java                 |
"""

CONDITION #2 : If condition #1 passes, a reset token of a registered user is sent to attacker controlled email (with embedded unicode character)

//define the source function and attacker controlled vector (which is the email address parameter)
val source = cpg.method.fullNameExact("org.baeldung.web.controller.RegistrationController.resetPasswordBad:org.baeldung.web.util.GenericResponse(javax.servlet.http.HttpServletRequest,java.lang.String)").parameter.evalType("java.lang.String")

//define email channel sink function name
val EMAIL_CHANNEL_SINK="org.springframework.mail.javamail.JavaMailSender.send:void(org.springframework.mail.SimpleMailMessage)"

//define the sink function that participates in the data flow
val sink = cpg.method.fullNameExact(EMAIL_CHANNEL_SINK).parameter.evalType("java.lang.String")

// Verify BUSINESS LOGIC FLAW check to determine if attack controller vector (email) is used in emailSend function, rather than the registered user email (determined after fetch from DB in step #1)
sink.reachableBy(source).flows.p

//results
res58: List[String] = List(
  """ __________________________________________________________________________________________________________________________________________________________________
 | tracked                                                       | lineNumber| method                     | file                                                   |
 |=================================================================================================================================================================|
 | userEmail                                                     | 134       | resetPasswordBad           | org/baeldung/web/controller/RegistrationController.java|
 | userEmail                                                     | 139       | resetPasswordBad           | org/baeldung/web/controller/RegistrationController.java|
 | userEmail                                                     | 198       | constructResetTokenEmailBad| org/baeldung/web/controller/RegistrationController.java|
 | userEmail                                                     | 201       | constructResetTokenEmailBad| org/baeldung/web/controller/RegistrationController.java|
 | userEmail                                                     | 213       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | userEmail                                                     | 217       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | param0                                                        | N/A       | setTo                      | org/springframework/mail/SimpleMailMessage.java        |
 | this                                                          | N/A       | setTo                      | org/springframework/mail/SimpleMailMessage.java        |
 | email                                                         | 217       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | email                                                         | 218       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | this                                                          | N/A       | setFrom                    | org/springframework/mail/SimpleMailMessage.java        |
 | this                                                          | N/A       | setFrom                    | org/springframework/mail/SimpleMailMessage.java        |
 | email                                                         | 218       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | email                                                         | 219       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | ret                                                           | 213       | constructEmailBad          | org/baeldung/web/controller/RegistrationController.java|
 | this.constructEmailBad("Reset Password",$r11,userEmail)       | 201       | constructResetTokenEmailBad| org/baeldung/web/controller/RegistrationController.java|
 | param1                                                        | N/A       | .assignment      | N/A                                                    |
 | param0                                                        | N/A       | .assignment      | N/A                                                    |
 | $r12                                                          | 201       | constructResetTokenEmailBad| org/baeldung/web/controller/RegistrationController.java|
 | $r12                                                          | 201       | constructResetTokenEmailBad| org/baeldung/web/controller/RegistrationController.java|
 | ret                                                           | 198       | constructResetTokenEmailBad| org/baeldung/web/controller/RegistrationController.java|
 | this.constructResetTokenEmailBad($r9,$r10,token,$l0,userEmail)| 139       | resetPasswordBad           | org/baeldung/web/controller/RegistrationController.java|
 | param1                                                        | N/A       | .assignment      | N/A                                                    |
 | param0                                                        | N/A       | .assignment      | N/A                                                    |
 | $r12                                                          | 139       | resetPasswordBad           | org/baeldung/web/controller/RegistrationController.java|
 | $r12                                                          | 139       | resetPasswordBad           | org/baeldung/web/controller/RegistrationController.java|
 | param0                                                        | N/A       | send                       | org/springframework/mail/javamail/JavaMailSender.java  |
"""
)

Safe Coding to prevent this business logic flaw

  1. Observe for anomalous volume of password resets (forgot password requests) initiated upon your application. An attacker is most likely enumerating your end point.
  2. Use two factor authentication (2FA) as a part of validation and reset functions.
  3. As John Gracey suggests, use punycode conversion as a part of your registration, validation and reset functions. Validate for both, local and domain part of email addresses.
  4. Continuously verify your entire fleet of applications in a CI/CD pipeline to ensure that none of the conditions above are violating baseline checks in any current and future releases.
  5. Send out password reset email ONLY to the original email address that was used to create the account and NOT to email address controlled by attacker.

ShiftLeft is an application security platform built over the foundational Code Property Graph that is uniquely positioned to deliver a specification model to query for vulnerable conditionsbusiness logic flaws and insider attacks that might exist in your application’s codebase.

If you’d like to learn more about ShiftLeft, please request a demo.

Stay Safe!