Security checks based on user-controlled input

Reliance on untrusted inputs in a security decision

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.userId with req.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 admin role 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.

References