29

I have a requirement to generate a one time use URL which should have the following features:

  1. As the URL query parameters may contain sensitive information, it should be encrypted (on top of https encryption).
  2. Once used, the URL cannot be used again.
  3. URLs have automatic expiry after a certain amount of time.
  4. It shall be possible for an administrator to revoke a valid URL. If the user at a later point in time tries to use this URL, he should see an appropriate error message.

To do this, I can think of two high level approaches:

  1. Generate a random number as the query parameter of the URL. Store the random number and the corresponding parameters (i.e. the real query parameters, expiration, revocation status, used status) in a database. When the user uses the URL check all the required pre-conditions and mark it as used before providing the real query parameters.
  2. Embed the real query parameters and the expiration timestamp as the query parameter of URL. Encrypt the URL with an algorithm such as AES256. However, I would still need to store the URL in a database so as to provide the revocation feature.

Based on the above I am leaning towards option 1 as all the logic is in a single place and it looks more secure. Is there any industry best practice to deal with this type of problem?

If it matters, this will be a REST-based web service hosted on IIS.

psmears
  • 900
  • 7
  • 9
Rao Nagaraj
  • 393
  • 1
  • 3
  • 6
  • 13
    Be aware that one-time-use URLs may not work everywhere. Some things might retrieve the resource before actually opening the URL in the browser. I think KDE did (or still does) that for a while. So if the response would e.g. be a session cookie, it is lost in some random tooling instead of ending up in the users browser :(. – Jonas Schäfer Apr 05 '18 at 14:19
  • 11
    Given what @JonasWielicki is saying about browser pre-fetch, the most effective way to deal with that is to make the action that "uses up" the one-time resource a POST request. Pre-fetch mechanisms usually restrict themselves to GET requests. You could do a POST with a form action, or XHR request. If there needs to be a link to the page, create a shell that exists just to make the POST request. – nbering Apr 05 '18 at 16:37
  • 4
    It's not just web browsers that pre-fetch URLs. Virus scanners and spam filters do as well. – Mark Apr 06 '18 at 00:33
  • 1
    Form the point of view of API architecture there's nothing wrong with the tools which might prefetch the URLs. By design a HTTP GET request is considered as a safe method which shall not alter the state of the requested resource. See: https://tools.ietf.org/html/rfc7231#section-4.2 – Noir Apr 06 '18 at 10:15

3 Answers3

26

Looks like you have a pretty good idea what you're doing.

The one-time link pattern is pretty common for things like email verification. Typically, you'd store the expiration date in a database and/or use a signed string in the URL which includes the expiry in the string-to-sign. These are just precautions to avoid trusting user input.

If you want to be really thorough, you can send them the actual random ID or token URL, and then store a hash in the database to avoid someone using the token if you have a data breach.

You're pretty vague about the context of the proposed AES-encrypted parameters. I would usually include sensitive information which is not required for the URL-routing in a message body, instead of the URL. That way, it doesn't appear in web server or proxy logs. AES-encrypted data may also push you over the URL length limit pretty quickly, depending how large the plaintext content is.

Edit: For completeness, Azure Storage SAS Tokens are a great example of a cryptographic method that requires no database record, and is revokable. Revocation is done by changing the service's API Key... which would revoke all tokens issued by the same key.

Given that I don't have a lot of information about the system in use, I would recommend the simpler database lookup, over any solution that requires cryptographically sophisticated measures.

Also, for cryptographic methods to work as a one-time link, some parameter known by the service, and included in the URL, must be changed when the link is used. This parameter doesn't have to be a database lookup, but there does need to be a persisted change. For example, if the link is a one-time upload URL, the presence of a file in the upload directory might invalidate the URL.

Lastly, the database-stored value is simpler in that there is only one source to check for validity. With the no-database solution, you'd need to check against the secret (ie. the encryption key), and also whatever factor invalidates the link after first use.

nbering
  • 3,988
  • 1
  • 21
  • 22
  • One additional advantage to solution 1 (which has little to do with security) is that with solution 2, existing URS:s might become unusable if required parameters change. – Guran Apr 05 '18 at 10:19
  • I quite agree. I was actually considering giving a comparison to Azure Storage SAS tokens, which to revoke them, you roll the storage account key. To get a description of that right for this case though, would have required more information, I feel. – nbering Apr 05 '18 at 11:28
10

To use symmetric crypto for an URL parameter would give you one advantage: It eliminates the need to do a database lookup. The downside is that you introduce a secret (the encryption key) that you need to protect and maintain. Since you will need to do the database lookup anyway for checking the revocation status, you don't get any upside. So I would just go for approach #1.

Make sure to use sufficiently long ID:s, and generate them with a CSPRNG. And as nbering says, make sure to hash them (without salt) to protect you in case of a breach.

Anders
  • 64,406
  • 24
  • 178
  • 215
  • 2
    You can still use a pepper, even if you do not use a hash, and if the request is linked to a specific user, the user ID itself can be used as salt. – Matthieu M. Apr 05 '18 at 18:56
  • 1
    @MatthieuM. Great point, never thought of the username thing before - smart. A random token should have so much entropy that just a single round of SHA256 without any spicing would be enough, though. – Anders Apr 05 '18 at 22:56
  • Side note: user ID should not equal username. Makes it difficult to facilitate username changes (for which there's a variety of reasons to allow), not to mention slower for comparisons. IDs on things like users (or, eg, StackOverflow questions) are something you'd typically perform a lot of comparisons against. But if we're having usernames changeable, that's something to keep in mind before using it as a salt... – Kat Apr 10 '18 at 19:37
4

Software giants like Google use a method of signed URLs

The premise is simple, you send a request with an encrypted string that matches the unencrypted side of the query. Then all you need to do is decrypt it on the server.

For example

example.com?file=file.ext&time={unixtimecode}&{encryptedstringthatmatchesfileandtimestamp}

The use of the encryption key is to ensure that the server/someone with access to the key made the request that a user can follow for a certain amount of time and that the request has not been tampered with. Revoking the string could be as simple as deleting a temporary encryption key meaning that the server wouldn't be able to decrypt the request, even if it was cached.

Tom K.
  • 7,913
  • 3
  • 30
  • 53
  • 2
    How does this answer the question if this is a best practice? This only gives an example use of option 2. – Eelke Apr 05 '18 at 13:14