13

What would be useful solutions for hiding true database object ID in URL for security purposes? I found that one of the solutions would be:

1) Using hashids open source project

2) Using something like same old md5 on creation of the object to generate hash and store it in database, then use it in url's and querying by them, but the drawback is that querying by auto-incremented primary keys (IDs) is faster than hashes. So I believe the possibility to hash/unhash would be better?

Also as I'm on Symfony, are there maybe bundles that I could not find or built in functionalities that would help?

Please tell me what you found useful based on your experiences.

Ryan Vincent
  • 4,483
  • 7
  • 22
  • 31
Dan Mironis
  • 711
  • 6
  • 14
  • 1
    If you have an index on the hash, it should be about as fast to query on it as the ID. – Barmar Sep 26 '15 at 10:19
  • 1
    Just curious why you are asking? E.g. If this is to beef up security... Maybe there's a bigger issue that needs to be resolved? – scunliffe Sep 26 '15 at 10:27
  • @scunliffe Actually as of now I don't see any serious security holes, ofcourse if user access object that don't belong to him application will throw an exception. Either way, I don't like and think is not good practice to show database ID in URL for that object, as accessing that object can only person to whom it belongs to, not like a blog post which is publicly accessible. – Dan Mironis Sep 26 '15 at 10:31
  • 1
    @DanMironis if this is a personal preference then that's fine, but as for "good practice" I don't think it is an issue if IDs are present... It's just a number. E.g. This question ID is `32795998` which is in the URL and almost every link on this page. It doesn't compromise security for you and I to see it because we don't have access to the actual database. – scunliffe Sep 26 '15 at 10:58
  • @scunliffe as I stated, in my situation there is a requirement that the resource users access (e.g domain.com/book/5), it need to belong to the current user that is logged in, and in your example about the question, it is *public*. So yeah, maybe you're right, that it's just my personal preference and it doesn't compromise security. :) – Dan Mironis Sep 26 '15 at 11:06
  • @scunliffe If you scramble the IDs in URLs you can prevent enumerating your URLs by HTTP spiders, it may be important in some use cases. – hijarian Sep 26 '15 at 11:29
  • Do you just want to hide it so that it is not directly readable from the URL or prevent anyone to find out the ID based on the URL? – Jimmy T. Sep 26 '15 at 11:35
  • @scunliffe It can be an issue. Firstly, you have a approximate about how many objects there are. Secondly, you can easily access the other objects. That can be a problem in some cases, even if the data is publicly accessible. – Jimmy T. Sep 26 '15 at 11:41
  • @JimmyT. Actually I would want both, not readable and prevent to find out what's the actual ID – Dan Mironis Sep 26 '15 at 12:33
  • Without obfuscation of IDs, company growth becomes public knowledge. It's easy to calculate how many users sign up every month if each user has a ```user_id``` that increments by 1 every time an account is created. – touch my body May 08 '18 at 14:25

3 Answers3

14

This question has been asked a lot, with different word choice (which makes it difficult to say, "Just search for it!"). This fact prompted a blog post titled, The Comprehensive Guide to URL Parameter Encryption in PHP .

What People Want To Do Here

Some encryption function is used to deterministically retrieve the ID

What People Should Do Instead

Use a separate column

Explanation

Typically, people want short random-looking URLs. This doesn't allow you much room to encrypt then authenticate the database record ID you wish to obfuscate. Doing so would require a minimum URL length of 32 bytes (for HMAC-SHA256), which is 44 characters when encoded in base64.

A simpler strategy is to generate a random string (see random_compat for a PHP5 implementation of random_bytes() and random_int() for generating these strings) and reference that column instead.

Also, hashids are broken by simple cryptanalysis. Their conclusion states:

The attack I have described is significantly better than a brute force attack, so from a cryptographic stand point the algorithm is considered to be broken, it is quite easy to recover the salt; making it possible for an attacker to run the encoding in either direction and invalidates property 2 for an ideal hash function.

Don't rely on it.

Dario Seidl
  • 4,140
  • 1
  • 39
  • 55
Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
  • "What people should do instead" seems like it would use a lot of memory, and databases can potentially have millions of records – mid Apr 22 '18 at 20:47
  • 1
    Have you benchmarked this to verify that "millions of records" following this scheme would actually create problems in a real-world system? – Scott Arciszewski Apr 23 '18 at 22:03
  • If the random selector also has to be unique for each record in the database, the record ID can also be concatenated to that random selector, unless the application checks random selectors it generates for uniqueness or the application can tolerate the risk of possibly generating the same random selector more than once. – Peter O. Jun 20 '19 at 17:16
  • You can also accept that random selectors fulfill the role of a random oracle, and therefore design your selectors to have a keyspace with a birthday bound versus the maximum number of possible rows in your database (e.g. 2^31-1 for signed 32-bit integers with MySQL) to result in a negligible collision probability. If you use `random_bytes(18)` to generate your selector, you have a 144-bit keyspace which has a 2^-41 birthday collision probability against a 32-bit unsigned int keyspace. – Scott Arciszewski Jun 21 '19 at 09:59
4
  1. Quote from the site:

Do you have a question or comment that involves "security" and "hashids" in the same sentence? Don't use Hashids.

  1. I'd use true encryption algorithm, like function openssl_encrypt (for example), or something like this. And encrypt ids when passing outside, decrypt when using in your code (like for db queries).

And I won't recommend storing ids in a base like any kind of encrypted "garbage", in my opinion its very inconvenient to hash your real ids. Keep it clean and pretty inside and encrypt for external display only.

Nagh
  • 1,757
  • 1
  • 14
  • 19
  • Oh, your taken quote make sense for my question :) I'm now thinking that it's more like an *obscurity* than a *security*. Using php built in true encryptor may be a better idea. – Dan Mironis Sep 26 '15 at 10:59
  • 1
    Please [don't use or recommend mcrypt](https://paragonie.com/blog/2015/05/if-you-re-typing-word-mcrypt-into-your-code-you-re-doing-it-wrong). – Scott Arciszewski Sep 26 '15 at 19:32
  • @ScottArciszewski it doesn't meant to be a recommendation, just an example. I agree with your reasons and update answer to set openssl as example instead – Nagh Sep 26 '15 at 23:13
1

Following your idea, you just need to cipher your IDs before writing the URL to HTML page and decipher them when processing those URLs.

  • If you want just security by obscurity, which is sufficient for, maybe 99% of curious people out there who likes to iterate over IDs in URLs, you use something simple like base64 or rot13. Of course, you can also precalculate those "public IDs" and store in the database, not encrypting each time the URL is being shown to end user.
  • If you want true security you have to encrypt them with some serious asymmetric cypher, storing both keys at your side, as you essentially talking with yourself and don't want a man-in-the-middle attack. This you will not be able to precalculate as at each encrypting there'll be different cyphertext, which is good for this cause.

In any case, you need something two-way, so if I were you I'd forget about word "hash", hashes are for purposes different from yours.

EDIT: But the solution which every blog out there uses for this task for several years already is just to utilize URL rewriting, converting, in your case, URLs like http://example.com/book/5 to URLs like http://example.com/rework-by-37signals. This will completely eradicate any sign of database ID from your URL.

Ideologically, you will need something which will uniquely map the request URL to your database content anyway. If you hide MySQL database IDs behind any layer of URL rewriting, you'll just make this rewritten URL a new ID for the same content. All you gain is protection from enumeration attacks and maybe SEF URLs.

hijarian
  • 2,159
  • 1
  • 28
  • 34
  • Could you explain what do you mean by converting `http://example.com/book/5` to `http://example.com/rework-by-37signals`? – Dan Mironis Sep 26 '15 at 12:29
  • @DanMironis You sound like you are using Symfony framework and haven't read [docs about routing](http://symfony.com/doc/current/book/routing.html) – hijarian Sep 26 '15 at 16:46
  • I just didn't understand what did you mean by word 'converting', because the example you wrote `http://example.com/book/5` to `http://example.com/rework-by-37signals` is not converting, but just a another selector, in first you're selecting by *id* and in second by *slug*, and this is not what I need, because I don't have unique column beside the ID. – Dan Mironis Sep 26 '15 at 21:25
  • @DanMironis In any case, the principle stay the same: before rendering the URL containing an ID you replace it with something which you and you alone will be able to convert back, be it some encrypted representation of original number or completely overwritten SEF URL. – hijarian Sep 27 '15 at 01:09