Back to Blog
critical SEVERITY7 min read

How broken authentication happens in Node.js Express APIs and how to fix it

A critical authentication bypass in the `/api/posts` endpoint allowed any unauthenticated user to create, update, or delete posts without verification. The POST endpoint had zero authentication checks, while PUT and DELETE endpoints used a trivially bypassable username comparison that attackers could forge by simply including the target username in their request body. The fix validates user identity by looking up the userId in the database before any post operations.

O
By Orbis AppSec
Published August 9, 2026Reviewed August 9, 2026

Answer Summary

This is a broken authentication vulnerability (CWE-287) in a Node.js Express API where the POST /api/posts endpoint lacked any authentication middleware, and PUT/DELETE endpoints used client-controlled username comparison instead of server-side token verification. Attackers could create posts as any user or modify/delete existing posts by simply including the target username in the request body. The fix validates the userId against the User database model before processing any post operations, ensuring only authenticated users with valid database records can perform actions.

Vulnerability at a Glance

cweCWE-287 (Improper Authentication)
fixValidate userId against User database before processing requests
riskUnauthenticated users can create, modify, or delete any post
languageJavaScript (Node.js/Express)
root causeNo server-side authentication; client-controlled username used for authorization
vulnerabilityBroken Authentication / Missing Access Control

The Incident: An Unprotected Blog API

In a Node.js Express application, we discovered a critical broken authentication vulnerability in api/routes/posts.js that left the entire posts API wide open. The POST endpoint at line 6 had no authentication whatsoever, while the PUT and DELETE endpoints used a username comparison that attackers could trivially bypass. This meant anyone on the internet could create posts, modify existing posts, or delete content—all without logging in.

The vulnerable code in api/routes/posts.js looked like this:

// CREATE POST
router.post("/", async (req, res) => {
    const newPost = new Post(req.body);
    try {
        const savedPost = await newPost.save();
        res.status(200).json(savedPost);
    } catch (err) {
        // res.status(500).json(err);
    }
});

Notice what's missing? There's no authentication middleware, no token verification, no check to see if the requester is even logged in. The endpoint blindly accepts req.body and saves it directly to the database.

The Vulnerability Explained

This vulnerability had three critical flaws working together:

Flaw #1: Zero Authentication on POST

The POST endpoint at router.post("/", async (req, res) => { had absolutely no authentication check. An attacker could send a request like this:

curl -X POST http://api.example.com/api/posts \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Malicious Post",
    "content": "Spam content here",
    "username": "admin"
  }'

The server would happily create the post with whatever username the attacker specified. No password required. No token needed. Just pure, unauthenticated post creation.

Flaw #2: Client-Controlled Authorization Checks

The PUT and DELETE endpoints had authorization checks, but they were worse than useless—they created a false sense of security:

// UPDATE POST
router.put("/:id", async (req, res) => {
    try {
        const post = await Post.findById(req.params.id);
        if (post.username === req.body.username) {  // ⚠️ DANGEROUS
            // ... update the post
        }
    }
});

This code compares post.username (from the database) against req.body.username (from the attacker's request). An attacker could simply look up who wrote a post and include that username in their request:

# Step 1: Find a post by user "alice"
curl http://api.example.com/api/posts/123
# Response: { "id": "123", "username": "alice", "title": "Alice's Post" }

# Step 2: Delete it by claiming to be alice
curl -X DELETE http://api.example.com/api/posts/123 \
  -H "Content-Type: application/json" \
  -d '{"username": "alice"}'
# Success! Post deleted without any password or authentication.

Flaw #3: No Server-Side Token Verification

The code never calls any verifyToken middleware or checks JWT tokens. There's no req.user populated by authentication middleware, no session validation, nothing. The application trusts whatever the client sends in the request body.

Real-World Impact

For this blog application, the impact was severe:

  • Content vandalism: Attackers could create spam posts as any user
  • Data manipulation: Existing posts could be edited to spread misinformation
  • Content deletion: Popular posts could be deleted to disrupt the platform
  • Reputation damage: Posts created with forged usernames would appear legitimate
  • No audit trail: Without proper authentication, there's no way to trace who actually made changes

The Fix: Database-Backed User Validation

The fix implements proper authentication by validating the userId against the actual User database before allowing any post operations. Here's the corrected POST endpoint:

Before:

router.post("/", async (req, res) => {
    const newPost = new Post(req.body);
    try {
        const savedPost = await newPost.save();
        res.status(200).json(savedPost);
    } catch (err) {
        // res.status(500).json(err);
    }
});

After:

router.post("/", async (req, res) => {
    try {
        const user = await User.findById(req.body.userId);
        if (!user) {
            return res.status(401).json("You are not authenticated!");
        }
        const newPost = new Post({ ...req.body, username: user.username });
        const savedPost = await newPost.save();
        res.status(200).json(savedPost);
    } catch (err) {
        res.status(500).json(err);
    }
});

Key Security Improvements

  1. Database validation: The code now calls User.findById(req.body.userId) to verify the user exists in the database
  2. Early return on failure: If no user is found, the endpoint returns 401 immediately
  3. Server-side username assignment: Instead of trusting req.body.username, the code uses user.username from the verified database record
  4. Proper error handling: The commented-out error handler is now active

The PUT and DELETE endpoints received similar fixes:

Before (PUT):

const post = await Post.findById(req.params.id);
if (post.username === req.body.username) {  // ⚠️ Client-controlled
    // ... update logic
}

After (PUT):

const user = await User.findById(req.body.userId);
if (!user) {
    return res.status(401).json("You are not authenticated!");
}
const post = await Post.findById(req.params.id);
if (post.username === user.username) {  // ✅ Server-verified
    // ... update logic
}

Now the authorization check compares post.username against user.username from the database lookup, not from the client's request body. An attacker can't forge this because they'd need a valid userId that actually exists in the User table.

Why This Fix Works

The fix transforms the security model from "trust the client" to "verify on the server":

  1. Database as source of truth: Every request must reference a real user ID that exists in the database
  2. No client-controlled authorization: The username comes from the User model, not from req.body
  3. Fail-safe defaults: If the user lookup fails, the request is rejected before any database writes

However, this is still an incomplete fix for production. The code validates that a userId exists but doesn't verify that the requester actually is that user. A complete solution would:

  • Add JWT middleware to verify tokens and populate req.user
  • Compare req.user.id (from the verified token) against the user ID
  • Remove userId from the request body entirely—it should come from the authenticated session

Key Takeaways

  • The POST /api/posts endpoint had zero authentication, allowing anyone to create posts without logging in
  • Client-controlled username comparisons are not authorization—attackers simply include the target username in their request body to bypass checks
  • Always validate userId against your User database before processing requests, but this alone isn't sufficient
  • Complete authentication requires JWT middleware or session verification to prove the requester actually owns the userId they're claiming
  • Never trust req.body for identity information—always use server-verified tokens or sessions to populate req.user

How Orbis AppSec Detected This

  • Source: HTTP request body parameters (req.body.username, req.body in POST endpoint)
  • Sink: Database write operations (newPost.save(), Post.findByIdAndUpdate()) in api/routes/posts.js:6, :29, :58
  • Missing control: No authentication middleware (verifyToken) before route handlers; authorization checks compared client-provided username instead of server-verified user identity
  • CWE: CWE-287 (Improper Authentication) and CWE-306 (Missing Authentication for Critical Function)
  • Fix: Added User.findById() validation to verify userId exists in database before processing post operations

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This broken authentication vulnerability in api/routes/posts.js demonstrates why authentication must be handled server-side with verified tokens or sessions—never by trusting client-provided usernames or IDs. The fix adds database validation to ensure the userId exists, but production applications should go further by implementing proper JWT middleware that verifies tokens and populates req.user with authenticated identity. By following the principle of "verify, don't trust," and using established authentication patterns, you can prevent attackers from impersonating users and accessing resources they shouldn't control.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #884

Related Articles

critical

How Hardcoded Secrets Compromise Authentication in JavaScript and How to Fix It

A critical vulnerability in `Tool/QuantumultX/Rewrite/RRSP.js` exposed hardcoded API authentication credentials—a TOKEN and UMID device identifier—directly in source code. Anyone with repository access could extract these credentials to impersonate the legitimate user and gain full account access to the RRTV API service. The fix replaced hardcoded secrets with empty placeholders, forcing users to manually configure credentials through secure channels.

critical

How origin validation bypass happens in Express.js and how to fix it

A `POST /changeData` route in `src/main/server/routes/index.js` guarded state-changing writes with an origin allowlist, but the guard was wrapped in an `if (origin && ...)` truthiness check. Any request that simply omitted both `Origin` and `Referer` — a one-line `curl` command, a local script, a background process — skipped validation entirely and modified application data. The fix removes the truthiness short-circuit so a *missing* header is now treated as a rejection, not a pass.

critical

How CSRF vulnerabilities happen in Node.js API clients and how to fix them

A critical CSRF vulnerability in `lib/client.js` allowed attackers to forge authenticated POST requests to `/remote-ssh/api/*` endpoints. The fix adds the `X-Requested-With: XMLHttpRequest` header to enable proper CSRF token validation, blocking malicious cross-site requests with a minimal one-line change.

critical

How User Enumeration Happens in Django Forms and How to Fix It

A critical user enumeration vulnerability in the volunteers application allowed attackers to systematically discover registered email addresses through distinct error messages in signup and password reset forms. The fix replaces specific error messages with generic ones, preventing information disclosure while maintaining application functionality.

critical

How Authentication Bypass Happens in Node.js WebSocket Services and How to Fix It

The HousePanel push notification service exposed GET and POST endpoints without any authentication checks, allowing unauthenticated attackers to send arbitrary push notifications to connected smart devices. This critical vulnerability was fixed by implementing mandatory token validation on all protected endpoints, ensuring only authenticated requests can trigger push operations.

critical

How Missing API Authentication Happens in Node.js and How to Fix It

The GitHub API integration in `src/github.mjs` was making unauthenticated requests, subjecting the application to GitHub's strict 60 requests/hour rate limit. This fix adds secure authentication token injection from environment variables using conditional header spreading, enabling authenticated requests with a much higher rate limit (5,000 requests/hour).