1
$\begingroup$

The scenario:

There is a web application where ownership check is time expensive so during the development all users were able to access all other user's objects (let's say apples) by changing the ids in the REST urls.

The user is authenticated by issuing a signed (not encrypted) JWT to the client in which one of the claims is a userId.

There was an initial attempt to make proper ownership checks on the server, but since it was very slow now everyone is looking for workarounds.

The team has made a decision - which I was against - to implement a following solution instead:

  • Instead of sending the user the ids of his apples, the user receives the ids encrypted symmetrically on the server. This should prevent the user to randomly guess ids (ids are autoincremented numbers).
  • When the user requests a specific apple, he requests it with the encrypted id he received, and the server decrypts the id and fetches the resource by it.
  • To prevent one user to reuse other users' ids, the encryption and decryption is done with a secret key that has the userId coming from the JWT token concatenated to it. This way you have to have a matching JWT token for your token to be decryptable.
  • The encryption is done by Jasypt's BasicBinaryEncryptor, where the plaintext id is converted to bytes and the resulting bytes are base64 encoded. Decryption is the same backwards.

My problem:

I do not have the knowledge to properly argue against this, apart from reciting things like "do not invent your own security", but this seems like a horrible disaster waiting to happen.

The JWT tokens for signed claims part is something that does not seem too fishy to me. They are stored in localstorage, the signature algorithm is set by the server, the secret is very long, there is expiration in them.

What seems problematic to me is that the cyphertext is not signed so if the user can forge the request somehow he still has access to all other user data.

Is it possible to somehow forge a request here? For example by messing around with the bits and see if the server throws an error (or what kind of error).

I'm looking for easy-to-understand reasons on why it is a bad idea to do this to present a solid argument to my colleagues (or be convinced that it is not).

$\endgroup$
1
$\begingroup$

First and foremost: Stop using JWT for sessions and why your cannot hack it into working.

I do not have the knowledge to properly argue against this, apart from reciting things like "do not invent your own security", but this seems like a horrible disaster waiting to happen.

So your primary question is how to convince your coworkers that this is a bad scheme?

If telling them they shouldn't be writing their own cryptography isn't enough, tell them instead how their custom solution is more expensive, complex and less secure than the alternatives. See below for alternatives.

What seems problematic to me is that the ciphertext is not signed so if the user can forge the request somehow he still has access to all other user data.

Signatures are asymmetric (expensive) and are (generally) larger than symmetric MACs. You'll be better off using symmetric solutions instead. Similarly encrypting is not authenticating and copy-pasting a ciphertext is not demonstrating an authority.

Is it possible to somehow forge a request here? For example by messing around with the bits and see if the server throws an error (or what kind of error).

If these object IDs are encrypted using CTR mode, then incrementing the ciphertext still gives you the next ID. If instead the object ID is encrypted using a PRP, then you can guess a random ID in $2^{b/2}/n$ guesses assuming a $b$-bit block-size and $n$ valid IDs exist. If you have 1 billion IDs, then this is only $18446744 \approx 2^{24}$ attempts. (can someone verify?)

Cryptographically this is weak. In practice you should have rate limiting and should slow down users who make repeated attempts for invalid IDs so this might not be too bad, but with only $18$ million guesses for $1$ billion possibilities this really depends on how sensitive you think these apples are. If they have no value, no one will try. If they are sensitive, it'll be only a matter of time.

I'm looking for easy-to-understand reasons on why it is a bad idea to do this to present a solid argument to my colleagues (or be convinced that it is not).

It is custom, slow, expensive, unverified and unnecessary!

Alternative 1: per-user isolated sets of apples

If apples cannot be shared between users then only display the ID relative to the user in question. I.e. If Alice has $3$ apples, they are $0$, $1$, and $2$. If Bob has $2$ apples, they are $0$, and $1$.

Additionally this can be bound to HTTP cookies such that malicious JavaScript cannot exfiltrate the JWT stored in LocalStorage. Better yet, this doesn't rely on JavaScript at all.

Alternative 2: capability-based sharing of apples

If apples can be shared between users, look into capabilities and macaroons.

A capability is an unforgeable, unguessable and communicable token of authority. You can only acquire a capability from someone who already holds it. Capabilities are stronger than cryptography, but they rely on trusted systems. Using keys-as-caps, cryptography can emulate a weaker form of capabilities based on knowledge (not possession). Macaroons are a cryptographic capability relying only on a secure keyed-hash (HMAC). No expensive asymmetric encryption or signatures.

Example macaroon (approximate example, see the paper for better definitions):

$$\begin{aligned} s &= \text{new_macaroon}(\text{server_secret}, \text{nonce})\\ &= (nonce, \text{HMAC}(\text{server_secret}, n))\\ a &= \text{add_caveat}(\text{authority}, \text{caveat})\\ &= (\text{authority}_0, \text{authority}_{n-1}, \dots, 0 \| \text{caveat}, \text{HMAC}(\text{authority}_n, 0 \| \text{caveat})\\ i &= \text{invoke}(\text{authority}, \text{message})\\ &= (\text{authority}_0, \text{authority}_{n-1}, \dots, 1 \| \text{message},\text{HMAC}(\text{authority}_n, 1 \| \text{message}) \end{aligned}$$

$s$ is an unrestricted credential. It is always valid. Technically the server could revoke it by the $nonce$, but this is not assumed. $a$ has additional caveats added. I.e. To expire the credential after some time, or to limit the objects or functions that may be reached or called. And finally $i$ invokes the authority to send some message to the server. If all caveats evaluate without failure, then the message is approved and evaluated.

Any time the un-invoked authority is shared we must use a secure transport to prevent a MiTM from learning the secret key. When invoking we reveal only public information and a proof binding the authority to the message. Conveniently this also makes CSRF tokens obsolete, but it does not prevent replays as-is.

Notice that macaroons do not assume the system has "users" or that revocation or expiration are required. The server is responsible to selecting the initial caveats, and the user may add any caveats they want when they share the credential. If Alice shares her credential with Bob to access an $\text{apple}_5$, then Bob can access only this apple and not any of Alice' other apples. Nor does this credential assume signatures.

Signatures only make sense when a large (or unknown) audience needs to verify the integrity offline. Your server is the only one producing and verifying these credentials correct? If your users need to authorize a third party service to perform some actions on their behalf, they share a provisioned credential with any caveats they want. Additionally, the user can include a special kind of caveat that pings back to their own server to ask for permission to perform some action. This enables the user to revoke the capabilities.

The users can add caveats and distribute them offline without interacting with your servers.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.