Business information leak - JWT - TypeScript

Business information leak - JWT - TypeScript

Need

Protection of sensitive business information within JWT

Context

  • Usage of TypeScript for type-checking and compiling JavaScript code
  • Usage of Express for building web applications and APIs
  • Usage of jsonwebtoken for generating and verifying JSON Web Tokens (JWT)

Description

Non compliant code

        import express from 'express';
import jwt from 'jsonwebtoken';

const app = express();

app.get('/login', (req, res) => {
  const username = req.query.username;
  const password = req.query.password;

  const token = jwt.sign({ username, password }, 'secretKey');

  res.json({ token });
});

app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];

  try {
    const decoded = jwt.verify(token, 'secretKey');
    const { username, password } = decoded;

    res.json({ username, password });
  } catch (error) {
    res.status(401).json({ message: 'Invalid token' });
  }
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
        
        

The vulnerability in this code lies in the way sensitive information, such as the username and password, is being stored and transmitted within the JSON Web Token (JWT).

In the /login route, the code takes the username and password from the request query parameters and signs them using the jwt.sign function. This means that the JWT generated will contain the username and password in plain text.

Later, in the /protected route, the code extracts the JWT from the Authorization header and attempts to verify it using the jwt.verify function. If the verification is successful, it proceeds to extract the username and password from the decoded JWT and sends them back in the response.

This approach poses a security risk because JWTs are typically used for authentication and should not contain sensitive information like passwords. By including the password in the JWT, an attacker who gains access to the token can easily obtain the user's password.

To mitigate this vulnerability, it is recommended to remove the sensitive information from the JWT and manage it securely on the server-side. Instead of including the username and password in the JWT payload, you should only include a unique identifier or any other non-sensitive information that can be used to identify the user.

Steps

  • Remove the sensitive information (username and password) from the JWT payload.
  • Store the sensitive information (username and password) securely on the server-side.
  • When generating the JWT, only include non-sensitive information in the payload.
  • When decoding the JWT, validate its signature and integrity.
  • Retrieve the sensitive information (username and password) from the server-side based on the authenticated user's identity stored in the JWT.
  • Update the '/protected' route to retrieve the sensitive information from the server-side instead of decoding it from the JWT payload.
  • Handle authentication and authorization securely on the server-side.

Compliant code

        import express from 'express';
import jwt from 'jsonwebtoken';

const app = express();

app.get('/login', (req, res) => {
  const username = req.query.username;
  const password = req.query.password;

  // Store the sensitive information securely on the server-side

  // Generate the JWT with non-sensitive information
  const token = jwt.sign({ username }, 'secretKey');

  res.json({ token });
});

app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];

  try {
    // Verify the JWT signature and integrity
    const decoded = jwt.verify(token, 'secretKey');
    const { username } = decoded;

    // Retrieve the sensitive information from the server-side
    const password = getPasswordByUsername(username);

    res.json({ username, password });
  } catch (error) {
    res.status(401).json({ message: 'Invalid token' });
  }
});

function getPasswordByUsername(username: string): string {
  // Retrieve the password from the server-side based on the username
  // Implement your logic here
  return 'password';
}

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
        
        

The fixed code addresses the vulnerability by removing sensitive information from the JWT and managing it securely on the server-side.

In the /login route, the username and password are obtained from the request query parameters. However, instead of storing them in the JWT, the sensitive information is securely stored on the server-side. Only non-sensitive information, such as the username, is included in the JWT. The JWT is generated using the jwt.sign function, which takes the username and a secret key as parameters.

In the /protected route, the token is extracted from the Authorization header of the request. The JWT signature and integrity are verified using the jwt.verify function, which takes the token and the secret key as parameters. If the verification is successful, the username is extracted from the decoded token. The sensitive information, in this case, the password, is retrieved from the server-side using the getPasswordByUsername function. The username and password are then sent back in the response.

The getPasswordByUsername function is responsible for retrieving the password from the server-side based on the username. This function should be implemented with appropriate logic to securely retrieve the password.

By removing sensitive information from the JWT and managing it securely on the server-side, the fixed code mitigates the risk of business information leakage.

References