Rainbow Table Attack with Jwt Tokens
How Rainbow Table Attack Manifests in JWT Tokens
JSON Web Tokens (JWT) are commonly signed with the HS256 algorithm, which relies on a shared secret known only to the issuer and the verifier. When that secret is low‑entropy—for example, a hard‑coded string like "secret" or a short password—an attacker who captures a valid token can attempt to recover the secret by comparing the token’s signature against a pre‑computed rainbow table of candidate secrets.
The attack proceeds in three steps:
- Token collection: The attacker obtains a JWT from a login response, a cookie, or an API header.
- Rainbow table lookup: Using a table that maps common secrets to their HS256 HMAC outputs for the token’s header and payload, the attacker checks whether any entry reproduces the observed signature.
- Token forgery: Once the secret is discovered, the attacker can sign arbitrary payloads (e.g., elevating a user role) and present them as legitimate tokens.
In code, the vulnerability often looks like this (Node.js, jsonwebtoken library):
const jwt = require('jsonwebtoken');
// Weak secret – easily guessable
const SECRET = 'myweaksecret';
function issueToken(user) {
return jwt.sign({ sub: user.id, role: user.role }, SECRET, { expiresIn: '1h' });
}
function verifyToken(token) {
return jwt.verify(token, SECRET); // will accept any token signed with SECRET
}
Because SECRET is short and static, an attacker can build a rainbow table of, say, the top 10 000 passwords and test each against the token’s signature in seconds. If the secret is found, the attacker can issue a token with role: "admin" and bypass authorization checks.
JWT Tokens-Specific Detection
Detecting a weak JWT secret requires checking whether the signing key can be guessed from a small candidate set. Manual inspection of source code or configuration is one approach, but automated scanning can catch the issue in running services.
middleBrick’s black‑box scanner includes an Input Validation check that attempts to forge JWTs using a list of common secrets (e.g., "secret", "123456", "mysecretkey"). When scanning an endpoint that returns a JWT, the scanner:
- Extracts the token from the response.
- Re‑signs the token’s header and payload with each candidate secret using HS256.
- If any re‑signed token matches the original signature, the scanner flags a Weak JWT Secret finding with severity high.
Example CLI usage:
# Install the middleBrick CLI (npm package)
npm i -g middlebrick
# Scan a public API endpoint that returns a JWT
middlebrick scan https://api.example.com/login
The output will contain a section like:
Finding: Weak JWT Secret
Severity: High
Description: The JWT returned by /login can be forged using a guessed secret.
Remediation: Replace the static low‑entropy secret with a cryptographically random key of at least 256 bits, or switch to an asymmetric algorithm (RS256/ECDSA).
Because middleBrick does not need agents or credentials, the test works against any publicly reachable API, making it suitable for continuous monitoring in CI/CD pipelines via the GitHub Action or for ad‑hoc checks from a developer’s terminal.
JWT Tokens-Specific Remediation
The fix is to ensure the signing key possesses sufficient entropy and is managed securely. Two recommended approaches are:
- Symmetric HS256 with a strong random secret – generate a 256‑bit (or larger) secret at deployment time and store it in an environment variable or secret manager.
- Asymmetric RS256/ECDSA – use a private key to sign tokens and a public key to verify; the private key never leaves the signer, eliminating the risk of secret guessing.
Below is a Node.js example that replaces the weak static secret with a strong random key loaded from the environment:
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Load secret from env; generate a fallback only for demo purposes
const SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('base64');
function issueToken(user) {
// HS256 with a strong secret
return jwt.sign({ sub: user.id, role: user.role }, SECRET, { algorithm: 'HS256', expiresIn: '1h' });
}
function verifyToken(token) {
return jwt.verify(token, SECRET, { algorithms: ['HS256'] });
}
module.exports = { issueToken, verifyToken };
If you prefer asymmetric keys, generate an RSA pair once and keep the private key secure:
const jwt = require('jsonwebtoken');
const fs = require('fs');
const PRIVATE_KEY = fs.readFileSync('keys/private.pem'); // RSA 2048‑bit or larger
const PUBLIC_KEY = fs.readFileSync('keys/public.pem');
function issueToken(user) {
return jwt.sign({ sub: user.id, role: user.role }, PRIVATE_KEY, { algorithm: 'RS256', expiresIn: '1h' });
}
function verifyToken(token) {
return jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] });
}
module.exports = { issueToken, verifyToken };
Key points to remember:
- Never hard‑code secrets in source code; use environment variables, Docker secrets, or a secret‑management service.
- Rotate keys periodically and invalidate old tokens via a token version or short expiry.
- Prefer RS256/ECDSA for services that issue tokens to many consumers, as the verification key can be distributed publicly without compromising security.
- After deploying the fix, re‑run middleBrick (via CLI, Dashboard, or GitHub Action) to confirm that the Weak JWT Secret finding no longer appears.