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, 2026•Reviewed 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

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

chrome.runtime.onMessage: Missing Sender Validation in Extension

A critical authentication flaw in a Chrome extension's message handling allowed arbitrary extensions and malicious web pages to trigger sensitive operations including script injection and tab manipulation. The vulnerability existed because chrome.runtime.onMessage listeners processed requests without verifying sender identity or origin, trusting any message source by default.

critical

Google Generative AI Keys Exposed in Client-Side Fetch URLs

A client-side AI model discovery utility was embedding Google Generative AI API keys directly in fetch request URLs, making them visible to any user inspecting network traffic or browser DevTools. The fix moves the key from the URL query parameter to a secure HTTP header, eliminating exposure while maintaining API authentication.

critical

boardroom Server Handler Missing Authentication on HTTP Endpoints

The boardroom server's `Handler` class, extending `SimpleHTTPRequestHandler`, exposed sensitive HTTP endpoints without any caller authentication. An attacker could exploit this by making cross-origin requests to internal ports through DNS rebinding, accessing `/alerts.json`, `/dismiss`, and `/events.js` without authorization. The fix adds `is_local_origin()` checks to both `do_GET()` and `do_POST()` methods.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.

critical

JWT Authentication Disabled Signature Validation in

A critical misconfiguration in JWT authentication explicitly disabled signature validation, allowing attackers to forge valid tokens with arbitrary claims and bypass authentication entirely. The fix re-enables signature validation on all incoming bearer tokens, restoring the security boundary of the authentication layer.