5

I have a MEAN stack app with a REST like api. I have two user types: user and admin. To sign the user in and keep the session i use jsonwebtoken jwt like this (simplified):

const jwt = require("jsonwebtoken");

//example user, normally compare pass, find user in db and return user
let user = { username: user.username, userType: user.userType };

const token = jwt.sign({ data: user }, secret, {
    expiresIn: 604800 // 1 week
});

To protect my express routes from I do this:

in this example it is a "get user" route, the admin is ofc allowed to get information about any given user. The "normal" user is only allowed to get information about him/her -self, why i compare the requested username to the username decoded form the token.

let decodeToken = function (token) {
    let decoded;
    try {
        decoded = jwt.verify(token, secret);
    } catch (e) {
        console.log(e);
    }
    return decoded;
}

// Get one user - admin full access, user self-access
router.get('/getUser/:username', (req, res, next) => {
    let username = req.params.username;
    if (req.headers.authorization) {
        let token = req.headers.authorization.replace(/^Bearer\s/, '');
        decoded = decodeToken(token);

        if (decoded.data.userType == 'admin') {
            //do something admin only
        } else if (decoded.data.username == username) {
            //do something user (self) only
        } else{
            res.json({ success: false, msg: 'not authorized' });
        }
    } else {
        res.json({ success: false, msg: 'You are not logged in.' });
    }
})

So my question is, how secure is this? Could someone manipulate the session token to swap the username to someone else's username? or even change the userType from user to admin?

My guess is. Only if they know the "secret" but is that enough security? the secret is after all just like a plain text password stored in the code. What is best practice?

Rasmus Puls
  • 3,009
  • 7
  • 21
  • 58
  • Have you run it to `let user = { username: user.username, userType: user.userType };`? IIUC, this defines a variable `user` which is initially `undefined`. Then it's time to evaluate `{...}` which involves accessing `user.username`. Looking up the `username` property on `undefined` fails with a *TypeError*. – Mike Samuel Jan 16 '18 at 16:11
  • I don't see how I have that problem, or I don't exactly understand your point. As my comment in the code explains, the user object, is only an example. Normally an user would be returned from the db, and if not an error would be handled. – Rasmus Puls Jan 16 '18 at 17:34
  • Please ignore. Your example code is confusing, but I think I understand what you were getting at. You do have a problem in `if (decoded.data.userType == 'admin')` since `decoded === undefined` if `jwt.verify` fails with an exception in `decodeToken`. Maybe consider initializing `decoded` to a structurally valid value that implies no authority in the `catch` block. Also, `decoded` should probably be declared with `let` in your handler. – Mike Samuel Jan 16 '18 at 17:45
  • Arhh, yeah. I think I get your point. Usually when jwt.verify fails, it generates a response saying something like "buffer error, secret must be of type String" ..... something like that. But, then it fails, and the "fragile" part of the code is not reached, which is the main concern. Still, thanks for pointing it out, i will try to provoke some jwt errors and look for solutions to that. – Rasmus Puls Jan 16 '18 at 17:51

5 Answers5

2

the secret is after all just like a plain text password stored in the code.

That's right. If secret is not kept secret, then an attacker can forge user objects and sign them using that secret and verify would not be able to tell the difference.

Putting secrets in code risks the secret leaking. It could leak via your code repository, because a misconfigured server serves JS source files as static files, or because an attacker finds a way to exploit a child_process call to echo source files on the server. Your users' security shouldn't depend on noone making these kinds of common mistakes.

What is best practice?

Use a key management system (KMS) instead of rolling your own or embedding secrets in code. Wikipedia says

A key management system (KMS), also known as a cryptographic key management system (CKMS), is an integrated approach for generating, distributing and managing cryptographic keys for devices and applications.

Often, how you do this depends on your hosting. For example, both Google Cloud and AWS provide key management services.

https://www.npmjs.com/browse/keyword/kms might help you find something suitable to your stack.

Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
  • This is indeed the answer to the key management issue. However, for smaller projects, it is often infeasible, which is very unfortunate, because storing the secret on the web server is a real risk. – Gabor Lengyel Jan 16 '18 at 18:57
  • @GaborLengyel, When you say "smaller projects" are you talking about ones that aren't operating inside a container like gCloud or AWS? Might Kubernetes be an option for those? – Mike Samuel Jan 16 '18 at 19:01
  • Unfortunately I'm not familiar enough with Kubernetes, but possibly yes, sounds like a reasonable idea! Thanks :) – Gabor Lengyel Jan 16 '18 at 19:10
1

This is extremely secure, especially if sent over HTTPS - then an attacker has no idea what your request payload looks like.

The only real danger is making sure you keep the SECRET safe, don't save on public git repos, lockdown access to any box on which the secret is stored. Use an encoded secret.

There are also other ways to harden your server. Consider using npm's popular helmet module.

danday74
  • 52,471
  • 49
  • 232
  • 283
  • Thanks, that was what I needed to know. I'm ssl encrypted so I guess it is good enough for my purpose with this app. I have used helmet previously, but this time I'm only using cors. Maybe I should consider adding helmet middleware if I encounter threats in the future. – Rasmus Puls Jan 16 '18 at 16:21
1

This is secure if your data packets are sent over HTTPS. If you want to add another layer of security what you can do is you can first encrypt the user details with the help of iron which uses 'aes-256-cbc' for encryption and then use that encrypted text and generate a token through JWT. In this way if a user somehow gain access to your token and go the website of JWT. He won't be able to recognize what's inside this token.

Again this is just to add an extra layer of security and it makes technically infeasible for a person to extract information because it's very time consuming yet add some extra security to our application.

Also make sure that all of your secrets(secret keys) are private.

Alqama Bin Sadiq
  • 358
  • 1
  • 4
  • 17
1

To answer your initial question "Could someone manipulate the session token to swap the username to someone else's username? or even change the userType from user to admin?"

JWT tokens are encrypted from the server-side and sent to the client in the form of a response. With that token, there are two things to keep in mind:

  1. Token is encrypted with a private key that only the server knows about
  2. The token contains, what is known as, a signature which validates the contents of the JWT payload (e.g. userType etc.)

For more information regarding the first point, please refer to the following answer.

For a better visual representation of JWT tokens and signatures, take a look at the following URL - https://jwt.io/


With this in mind, you must make sure that:

  • Your secret key is never exposed at any point in time to external services/clients. Ideally this key is not even hard-coded in your codebase (ask, should you want me to elaborate on this).

  • You don't have any logical flaws in your endpoints.

From a design perspective, I would much rather segregate any endpoints related to admin-space into their own endpoints however at a quick glance, your code seems to be fine. :-)


On a side-note that may assist you, if you don't have much experience in Web Application security, I would recommend checking out some automated scanners such as Acunetix, Burp (hybrid) and so on. Whilst not perfect by any means, they are quite capable of detecting a good amount of behavioral vulnerabilities (as in, ones a malicious actor would normally exploit).

Juxhin
  • 5,068
  • 8
  • 29
  • 55
  • Note that the jwt token is *not* encrypted, only signed. Contents are visible to the user, only its integrity (authenticity) is protected. – Gabor Lengyel Jan 16 '18 at 18:59
1

Some potential vulnerabilities in the code above:

  • Variable username comes from the url, but usertype is asserted from the token in the router action. An audit log for example could be corrupted if it took the userid from the username variable, because that is not authentic and can be anything sent by the user.

  • Replay is an issue. As the token is valid for one week, it is impossible to revoke admin rights for example until the token expires. (A malicious user could replay the previous admin token.)

  • Similarly, you cannot terminate (force logout) any session until tokens expire. This is a common issue in such stateless designs.

  • Null reference after token decode as pointed out by others. In node.js that is more like a weakness than a vulnerability I'd say, I think it's not exploitable.

  • The secret on the server is extremely valuable and can be used to impersonate any user. It is very hard to protect such a secret in systems with high security requirements.

So contrary to another answer, this is far from being 'extremely secure', but can be reasonably secure for many purposes.

Gabor Lengyel
  • 14,129
  • 4
  • 32
  • 59
  • first point. I don't think so, or I am missing something. Yes the "anyone" can send whatever username he/she wants in the url parameter, but note that it is compared to the username decoded from the token. I.e a user will never be able to access any other user that who is hidden in the token. forget about userid, they are not used for any comparison. ;) I'm aware of the session expiration, this is actually only for dev reasons, will be shortened to 1 day at launch. your last point, yes this was one of my concerns when posting this q. I'm desired to protect this a bit more. Thanks for you reply – Rasmus Puls Jan 16 '18 at 20:01