Description
Authentication or authorization is vulnerable when it trusts unverified cookies, headers, query values, or route parameters. Comparing two user-controlled values does not establish identity or permission: an attacker can set a role=admin cookie or make a cookie's userId match the URL's userId.
Potential impact
- Authentication bypass or impersonation
- Privilege escalation or access to other users' resources
- Exposure of personal, payment, or internal configuration data
- Unauthorized use of administrative operations
Remediation
- Use trusted server-side information, such as sessions (
req.session), verified signed cookies (req.signedCookies), validated JWT claims, or database records. - Do not infer identity or permission solely by comparing
req.cookies.userIdwithreq.params.userId. - Resolve the authenticated user's identity and permissions on the server, then apply the server's authorization policy.
- Verify cookie or token integrity. Use signed cookies or server sessions and validate JWT signatures rather than trusting plain input.
- Apply consistent authentication and authorization middleware to sensitive routes.
- Grant only necessary permissions and use allow-lists.
Examples
Before
javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
app.use(cookieParser()); // Unsigned: clients can change cookie values.
// Before: use client-controlled cookies and queries for authorization.
app.get("/admin", (req, res) => {
// The client can choose both role and user.
if (req.cookies.role === "admin" && req.query.user === req.cookies.user) {
return res.send("admin page");
}
return res.status(403).send("forbidden");
});
After
Supply a high-entropy secret securely through COOKIE_SECRET. This excerpt verifies a cookie issued after real authentication; cookie issuance and a production database connection are omitted.
javascript
const express = require("express");
const cookieParser = require("cookie-parser");
const app = express();
const cookieSecret = process.env.COOKIE_SECRET;
if (!cookieSecret || cookieSecret.length < 32) {
throw new Error("COOKIE_SECRET must contain at least 32 characters");
}
app.use(cookieParser(cookieSecret)); // Enable signed cookies.
// Illustrative user lookup (mock implementation).
async function findUserById(id) {
// Use a real database lookup in production.
const mock = { id: "u123", role: "admin" };
return id === "u123" ? mock : null;
}
// After: authorize using verified cookies and server-controlled data.
app.get("/admin", async (req, res) => {
const userId = req.signedCookies.userId; // Signature-verified identifier.
if (!userId) return res.status(401).send("login required");
const user = await findUserById(userId); // Check permissions against server-side data.
if (!user || user.role !== "admin") return res.status(403).send("forbidden");
return res.send("admin page");
});
Explanation:
- Before: The client controls both cookie and query values, so it can choose matching values and an
adminrole to bypass the check. - After: Verify the signed identifier, then retrieve the user and role from server-controlled data. Matching unverified client inputs is no longer treated as authorization.