Understanding Broken Access Control for web and mobile apps
Access control systems are critical for the security of web and mobile applications. They ensure that users can only access resources that they are permitted to following the principal of least privilege. This is an information security concept which maintains that a user or entity should only have access to the specific data, resources and applications needed to complete a required task. While this may seem obvious to some developers, broken access control remains a significant vulnerability in many apps. It topped the OWASP top ten report as the most frequently reported security issue, affecting numerous applications and leading to unauthorized access and data breaches. There are a few simple nodejs code examples included below that illustrate some potential issues.
An image reminiscant of broken access control
How is this an issue?
Correct Access control implementations ensure that a user cannot perform actions outside their intended permissions. This could be due to misconfigurations, insufficient security practices or flawed application logic. Such vulnerabilities can lead to unauthorized information disclosure, modification or destruction.
Common Vulnerabilities
Some common issues include:
  • Failure to validate a session token has not been tampered with (to elevate permissions, etc).
  • Failure to check that a token is still valid (reusing tokens is common in replay attacks)
  • Direct object references that allow unauthorized access to user resources
  • Misconfigurations allowing unauthorized API access
Failure to Authorize properly
There are a couple of issues with the following code that handles an HTTP request to update an organization name. This would typically be done with a PATCH or PUT HTTP request. See how many of them you can spot:

export default async function handle(req: NextApiRequest, res: NextApiResponse) {
  const { orgId, name } = req.body

  try {
    const result = await prisma.org.update({
      where: { id: orgId },
      data: { name },
    })

    return res.status(StatusCode.SuccessOK).json(result)
  } catch (e) {
    return respondWithError(req, res, StatusCode.ServerErrorInternal, e.message)
  }
}
There are several issues with this piece of code.

The code does not include any checks to ensure that the request is coming from an authenticated user. This is a basic requirement to secure API endpoints. There is no authorization logic to verify whether the authenticated user has the necessary permissions to update the organization details. Without these checks, any user (authenticated or not) could potentially modify data they should not have access to, leading to unauthorized data modification. Missing Validation on orgId and name:

The code does not perform any validation on the inputs orgId and name. Ensuring that orgId is a valid and existing organization identifier in your database is crucial. Similarly, validating name to ensure it meets certain criteria (e.g., not empty, within character limits, etc.) is important to maintain data integrity. Input validation is also a part of securing an application against malicious input that could be used in injection attacks, however ORM (Object-Relational Mapping) frameworks like Prisma typically handle SQL injections well.

Error Handling Exposes Too Much Information:

The error handling in the catch block may potentially return system-generated error messages to the client. It is better practice to log detailed errors on the server and return generic error messages to the client to avoid giving attackers insights into the backend systems or database schema.
Validating session token and Enforcing User Permissions
Here’s how you can correct the above code to include proper JWT validation and access control checks:

export default async function handle(req: NextApiRequest, res: NextApiResponse) {
  const session = await getServerSession(req, res, authOptions)
  if (!session) return respondWithError(req, res, StatusCode.ClientErrorForbidden, NOT_ALLOWED_ERROR_MESSAGE)

  const { orgId, userRights } = session
  const editEnabled = userRights.includes(UserRightEnum.ADMIN))
  if (!editEnabled) return respondWithError(req, res, StatusCode.ClientErrorForbidden, NOT_ALLOWED_ERROR_MESSAGE)

  try {
    const { name } = req.body
    if (!validatedOrganizationName(name)) return respondWithError(req, res, StatusCode.ClientErrorForbidden, ORG_NAME_INVALID_ERROR_MESSAGE)
    
    const result = await prisma.org.update({
      where: { id: orgId as string },
      data: { name },
    })
    if (!result) return respondWithError(req, res, StatusCode.ServerErrorInternal, ORG_UPDATE_FAIL_ERROR_MSG)

    return res.status(StatusCode.SuccessOK).json({ message: 'ok', error: false })
  } catch (e) {
    console.error(`Failed to update org ${orgId}. Message details: ${(e as Error).message}`)
    return respondWithError(req, res, StatusCode.ServerErrorInternal, ORG_UPDATE_FAIL_ERROR_MSG)
  }
}
This code has several improvements over the original implementation.

First, observe that the session token is checked. It is always better to use heavily used libraries for this kind of thing and follow the recommended / best practices. I usually check the github page for the number of stars and the npm package website for the number of weekly downloads. In this case, we use a library called next-authto do this for us. The error message is a constant called NOT_ALLOWED, which is generic. We could add a console.error unless we were worried about verbosity in the error logs.

Second, observe that we obtain the orgId and userRights from the session object and not the object passed in from the user. Next we double check that this user has the rights to update the organization name. In this case only users with the ADMIN role can update the name.

Next, we check that the organization name is valid with a function. This would typically check the type of the input (e.g. ensuring it is a string), ensure that it is a non-empty string and that it conforms to whatever other rules exist for valid organization names.

Finally, we ensure that a result was returned when updating the object and that any messages returned through the API response have minimal information. In this case when successfully updating the name we do not need to return any other information. Some APIs will return the object that was updated, but that is not necessary in this case.
Conclusion
Adopting comprehensive access control checks as demonstrated can help safeguard your applications from unauthorized access and potential security threats. Developers should continually refer to third party library documentation and resources like the OWASP's Top 10 List to stay updated on security best practices and integrate security into their development processes. Remember, security is not a one-time effort but a continuous process of improvement and adaptation to new threats.
Enter your email below and someone from our team will reach out to you. Also feel free to set up a meeting on Calendly.
© 2024 Aurelius Solutions