JWT Weak HMAC Secret Vulnerability
Weak HS256 secrets can be brute-forced offline with hashcat once an attacker obtains any valid JWT. Learn what makes a secret weak, how the attack works, and how to generate a strong secret.
HS256, HS384, and HS512 sign tokens with a shared secret. That secret is the only barrier between an attacker and token forgery. If the secret is weak, it can be brute-forced offline once an attacker obtains any valid JWT: no server contact is needed after capturing a single token. This is one of the most common JWT security findings in penetration tests.
How the attack works
The attacker captures one valid HS256 JWT (from a log, a proxy, a compromised client, a URL parameter). They then run hashcat in JWT mode (mode 16500) with a dictionary like rockyou.txt. On a modern GPU, hashcat tests hundreds of millions of candidate secrets per second against the captured token. A weak secret: a dictionary word, a common password, the literal example from the JWT website, a UUID, or any human-memorable phrase: falls in seconds to minutes. A 32-byte cryptographically random secret would require longer than the age of the universe with current hardware.
The fix
Generate secrets with a CSPRNG: at least 32 bytes for HS256, 48 for HS384, 64 for HS512. Not a password, not a UUID, not a memorable phrase. Use `openssl rand -base64 32`, `crypto.randomBytes(32)` in Node.js, `secrets.token_urlsafe(32)` in Python, or a dedicated generator. Switching from HS256 to RS256, PS256, or ES256 eliminates this entire vulnerability class: asymmetric algorithms use key pairs, so the private key is never shared and not derivable from the public key, leaving nothing to brute-force.
Code examples
The attack (hashcat against a captured HS256 JWT)
# Dictionary attack against an HS256 JWT
hashcat -a 0 -m 16500 captured_token.txt rockyou.txt
# With rules to try variations (capitalization, numbers, leetspeak)
hashcat -a 0 -m 16500 captured_token.txt rockyou.txt -r best64.rule
The fix: generate a strong secret
# OpenSSL: works on Linux, macOS, and Windows with Git Bash
openssl rand -base64 32
# Example output: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
// Node.js
const crypto = require("crypto");
const secret = crypto.randomBytes(32).toString("base64url");
# Python
import secrets
secret = secrets.token_urlsafe(32)
Frequently asked questions
-
How long should a JWT HS256 secret be?
At least 32 bytes (256 bits) of cryptographically random data from a CSPRNG: not a password, UUID, or memorable phrase. Generate it with openssl rand -base64 32, crypto.randomBytes(32) in Node.js, or secrets.token_urlsafe(32) in Python. A 32-byte random secret is effectively unbreakable with current hardware; a weak secret (dictionary word, common password, the JWT website example) can be brute-forced offline with hashcat in seconds once an attacker captures any valid token.
-
How does an attacker crack a JWT secret?
They capture one valid HS256 JWT (from a log, proxy, compromised client, or URL parameter), then run hashcat in JWT mode (hashcat -a 0 -m 16500 captured_token.txt rockyou.txt). No server contact is needed after capture: the attack is entirely offline. On a modern GPU, hashcat tests hundreds of millions of candidate secrets per second. A weak secret falls in seconds to minutes; a 32-byte cryptographically random secret would take longer than the age of the universe.
-
How do I eliminate the weak-secret vulnerability entirely?
Switch from HS256 to an asymmetric algorithm: RS256, PS256, or ES256. Asymmetric algorithms use key pairs: the private key signs and is never shared; the public key verifies and is public. There is nothing to brute-force, because the private key is not derivable from the public key. The tradeoff is slightly more complex key management (JWKS, key rotation) but the entire offline-brute-force vulnerability class is eliminated.