Introduction

Insecure Direct Object Reference (called IDOR from here) occurs when a application exposes a reference to an internal implementation object. Using this way, it reveals the real identifier and format/pattern used of the element in the storage backend side. The most common example of it (although is not limited to this one) is a record identifier in a storage system (database, filesystem and so on).

IDOR is referenced in element A4 of the OWASP Top 10 in the 2013 edition.

Context

IDOR do not bring a direct security issue because, by itself, it reveals only the format/pattern used for the object identifier. IDOR bring, depending on the format/pattern in place, a capacity for the attacker to mount a enumeration attack in order to try to probe access to the associated objects.

Enumeration attack can be described in the way in which the attacker build a collection of valid identifiers using the discovered format/pattern and test them against the application.

For example:

Imagine an HR application exposing a service accepting employee ID in order to return the employee information and for which the format/pattern of the employee ID is the following:

  1. EMP-00000
  2. EMP-00001
  3. EMP-00002
  4. ...

Based on this, an attacker can build a collection of valid ID from EMP-00000 to EMP-99999.

To be exploited, an IDOR issue must be combined with an Access Control issue because it’s the Access Control issue that “allow” the attacker to access to the object for which he have guessed the identifier through is enumeration attack.

Additional remarks

From Jeff Williams:

Direct Object Reference is fundamentally a Access Control problem. We split it out to emphasize the difference between URL access control and data layer access control. You can’t do anything about the data-layer problems with URL access control. And they’re not really input validation problems either. But we see DOR manipulation all the time. If we list only “Messed-up from the Floor-up Access Control” then people will probably only put in SiteMinder or JEE declarative access control on URLs and call it a day. That’s what we’re trying to avoid.

From Eric Sheridan:

An object reference map is first populated with a list of authorized values which are temporarily stored in the session. When the user requests a field (ex: color=654321), the application does a lookup in this map from the session to determine the appropriate column name. If the value does not exist in this limited map, the user is not authorized. Reference maps should not be global (i.e. include every possible value), they are temporary maps/dictionaries that are only ever populated with authorized values.

“A direct object reference occurs when a developer exposes a reference to an internal implementation object, such as a file, directory, database record, or key, as a URL or form parameter.”

I’m “down” with DOR’s for files, directories, etc. But not so much for ALL databases primary keys. That’s just insane, like you are suggesting. I think that anytime database primary keys are exposed, an access control rule is required. There is no way to practically DOR all database primary keys in a real enterprise or post-enterprise system.

But, suppose a user has a list of accounts, like a bank where database id 23456 is their checking account. I’d DOR that in a heartbeat. You need to be prudent about this.

Objective

This article propose an idea to prevent the exposure of real identifier in a simple, portable and stateless way because the proposal need to handle Session and Session-less application topologies.

Proposition

The proposal use a hash to replace the direct identifier. This hash is salted with a value defined at application level in order support topology in which the application is deployed in multi-instances mode (case for production).

Using a hash allow the following properties:

  • Do not require to maintain a mapping table (real ID vs front end ID) in user session or application level cache.
  • Makes creation of a collection a enumeration values more difficult to achieve because, even if attacker can guess the hash algorithm from the ID size, it cannot reproduce value due to the salt that is not tied to the hidden value.

This is the implementation of the utility class that generate the identifier to use for exchange with the front end side:

  1. import javax.xml.bind.DatatypeConverter;
  2. import java.io.UnsupportedEncodingException;
  3. import java.security.MessageDigest;
  4. import java.security.NoSuchAlgorithmException;
  5. /**
  6. * Handle the creation of ID that will be send to front end side
  7. * in order to prevent IDOR
  8. */
  9. public class IDORUtil {
  10. /**
  11. * SALT used for the generation of the HASH of the real item identifier
  12. * in order to prevent to forge it on front end side.
  13. */
  14. private static final String SALT = "[READ_IT_FROM_APP_CONFIGURATION]";
  15. /**
  16. * Compute a identifier that will be send to the front end and be used as item
  17. * unique identifier on client side.
  18. *
  19. * @param realItemBackendIdentifier Identifier of the item on the backend storage
  20. * (real identifier)
  21. * @return A string representing the identifier to use
  22. * @throws UnsupportedEncodingException If string's byte cannot be obtained
  23. * @throws NoSuchAlgorithmException If the hashing algorithm used is not
  24. * supported is not available
  25. */
  26. public static String computeFrontEndIdentifier(String realItemBackendIdentifier)
  27. throws NoSuchAlgorithmException, UnsupportedEncodingException {
  28. String frontEndId = null;
  29. if (realItemBackendIdentifier != null && !realItemBackendIdentifier.trim().isEmpty()) {
  30. //Prefix the value with the SALT
  31. String tmp = SALT + realItemBackendIdentifier;
  32. //Get and configure message digester
  33. //We use SHA1 here for the following reason even if SHA1 have now potential collision:
  34. //1. We do not store sensitive information, just technical ID
  35. //2. We want that the ID stay short but not guessable
  36. //3. We want that a maximum of backend storage support the algorithm used in order to compute it in selection query/request
  37. //If your backend storage supports SHA256 so use it instead of SHA1
  38. MessageDigest digester = MessageDigest.getInstance("sha1");
  39. //Compute the hash
  40. byte[] hash = digester.digest(tmp.getBytes("utf-8"));
  41. //Encode is in HEX
  42. frontEndId = DatatypeConverter.printHexBinary(hash);
  43. }
  44. return frontEndId;
  45. }
  46. }

This is the example of services using the front identifier:

  1. /**
  2. * Service to list all available movies
  3. *
  4. * @return The collection of movies ID and name as JSON response
  5. */
  6. @RequestMapping(value = "/movies", method = GET, produces = {MediaType.APPLICATION_JSON_VALUE})
  7. public Map<String, String> listAllMovies() {
  8. Map<String, String> result = new HashMap<>();
  9. try {
  10. this.movies.forEach(m -> {
  11. try {
  12. //Compute the front end ID fof the current element
  13. String frontEndId = IDORUtil.computeFrontEndIdentifier(m.getBackendIdentifier());
  14. //Add the computed ID and the associated item name to the result map
  15. result.put(frontEndId, m.getName());
  16. } catch (Exception e) {
  17. LOGGER.error("Error during ID generation for real ID {}: {}", m.getBackendIdentifier(),
  18. e.getMessage());
  19. }
  20. });
  21. } catch (Exception e) {
  22. //Ensure that in case of error no item is returned
  23. result.clear();
  24. LOGGER.error("Error during processing", e);
  25. }
  26. return result;
  27. }
  28. /**
  29. * Service to obtain the information on a specific movie
  30. *
  31. * @param id Movie identifier from a front end point of view
  32. * @return The movie object as JSON response
  33. */
  34. @RequestMapping(value = "/movies/{id}", method = GET, produces = {MediaType.APPLICATION_JSON_VALUE})
  35. public Movie obtainMovieName(@PathVariable("id") String id) {
  36. //Search for the wanted movie information using Front End Identifier
  37. Optional<Movie> movie = this.movies.stream().filter(m -> {
  38. boolean match;
  39. try {
  40. //Compute the front end ID for the current element
  41. String frontEndId = IDORUtil.computeFrontEndIdentifier(m.getBackendIdentifier());
  42. //Check if the computed ID match the one provided
  43. match = frontEndId.equals(id);
  44. } catch (Exception e) {
  45. //Ensure that in case of error no item is returned
  46. match = false;
  47. LOGGER.error("Error during processing", e);
  48. }
  49. return match;
  50. }).findFirst();
  51. //We have marked the Backend Identifier class field as excluded
  52. //from the serialization
  53. //So we can send the object to front end through the serializer
  54. return movie.get();
  55. }

This is the value object used:

  1. public class Movie {
  2. /**
  3. * We indicate to serializer that this field must never be serialized
  4. *
  5. * @see "https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/
  6. * jackson/annotation/JsonIgnore.html"
  7. */
  8. @JsonIgnore
  9. private String backendIdentifier;
  10. ...
  11. }

Sources of the prototype

Github repository.