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

Prevention & Best Practices

1. Use Authentication Middleware

Express applications should use middleware like passport.js or custom JWT verification:

const verifyToken = (req, res, next) => {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) return res.status(401).json("No token provided");

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;  // Populate req.user with verified data
        next();
    } catch (err) {
        return res.status(403).json("Invalid token");
    }
};

router.post("/", verifyToken, async (req, res) => {
    // req.user is now verified and trustworthy
});

2. Never Trust Client-Provided Identity

Any data in req.body, req.query, or req.params can be forged. Identity information must come from:

  • Verified JWT tokens decoded server-side
  • Server-side sessions looked up by session ID
  • Database queries using authenticated user IDs

3. Implement Role-Based Access Control (RBAC)

For resource ownership checks:

const post = await Post.findById(req.params.id);
if (post.userId.toString() !== req.user.id) {
    return res.status(403).json("Not authorized to modify this post");
}

4. Use Security Linters

Tools like ESLint with security plugins can catch missing authentication:

npm install --save-dev eslint-plugin-security

Configure rules to flag routes without authentication middleware.

5. Follow OWASP API Security Guidelines

The OWASP API Security Top 10 lists broken authentication as API1:2023. Key recommendations:

  • Implement proper authentication mechanisms
  • Use standard authentication solutions (OAuth 2.0, OpenID Connect)
  • Don't reinvent authentication—use proven libraries
  • Implement rate limiting to prevent brute force attacks

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.

References

Frequently Asked Questions

What is broken authentication in REST APIs?

Broken authentication occurs when an API endpoint fails to verify the identity of the requester before processing sensitive operations. This can mean missing authentication middleware entirely, or using client-controlled data (like usernames in request bodies) instead of server-verified tokens or session data.

How do you prevent broken authentication in Express.js?

Implement authentication middleware that verifies JWT tokens or session cookies before protected routes, validate user identity against your database using server-side data only (never trust client-provided usernames or IDs), and use authorization checks that compare the authenticated user's ID from the token against resource ownership.

What CWE is broken authentication?

Broken authentication is classified as CWE-287 (Improper Authentication). Related CWEs include CWE-306 (Missing Authentication for Critical Function) and CWE-863 (Incorrect Authorization).

Is checking username in the request body enough to prevent unauthorized access?

No. Client-provided data like usernames in request bodies can be trivially forged by attackers. Always validate against server-side authenticated data (like a verified JWT token's user ID) and look up the actual user record in your database to confirm identity.

Can static analysis detect broken authentication?

Yes. Advanced static analysis tools can detect missing authentication middleware on sensitive endpoints, identify authorization checks that rely on client-controlled input, and flag endpoints that lack proper token verification before database operations.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #884

Related Articles

medium

How Insufficient Password Hashing Cost Factor Happens in Node.js and How to Fix It

A bcrypt password hashing implementation in the User.js model was using a cost factor of 10, which falls below OWASP's 2024 recommendation of 12 for applications handling sensitive data. This fix upgrades the salt rounds from 10 to 12, increasing the computational work required to crack passwords by approximately 4x, significantly improving protection against brute-force attacks on this e-commerce platform.

critical

How missing authentication checks happen in React route handlers and how to fix it

A critical vulnerability in ManageMembers.jsx and Settings.jsx allowed any user with network access to perform privileged operations like adding, editing, and deleting members without authentication. The fix implements route-level authentication checks using React Router's Navigate component to redirect unauthenticated users to the login page.

medium

How OAuth token audience bypass happens in Node.js serverless functions and how to fix it

A critical OAuth authentication vulnerability in a Netlify serverless function allowed any valid Google OAuth token—even those issued to completely different applications—to authenticate successfully. The fix adds proper audience (aud) claim verification using Google's tokeninfo endpoint to ensure only tokens issued specifically for this application are accepted.

critical

How insecure nonce generation with Math.random() happens in Node.js HTTP Digest authentication and how to fix it

A critical vulnerability was discovered in `lib/cam.js` where the HTTP Digest authentication client nonce (cnonce) was generated using `Math.random().toString(36)` — a cryptographically insecure source of randomness. An attacker observing authentication exchanges could predict future cnonce values and forge valid authentication responses. The fix replaces this with `crypto.randomBytes(4).toString('hex')`, providing cryptographically secure random values.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.