← Todas las noticias
security4 min de lectura

HTTP parameter pollution in AI-generated Express.js APIs

Por Seaworthy · 2 de julio de 2026

Red padlock resting on a black computer keyboard, representing web security

Photo by FlyD on Unsplash

There is a class of vulnerability that has nothing to do with complex cryptography or obscure browser quirks. It comes from a gap between what developers assume about HTTP and what the spec actually defines. HTTP parameter pollution sits squarely in that gap, and AI-generated Express.js code walks right into it.

Send GET /api/users?role=guest&role=admin to an Express endpoint. What does req.query.role contain? Not 'admin'. Not 'guest'. It contains ['guest', 'admin']. An array. And most validation logic written by an LLM was never built to handle that.

How Express parses duplicate query string keys

Express delegates query string parsing to the qs library. qs is consistent: when it sees the same key more than once, it collects the values into an array. This is documented behavior, not a bug. The bug is in every route handler that assumes req.query.anything is a string.

The transformation is silent:

?role=admin                 →  req.query.role === 'admin'            (string)
?role=guest&role=admin      →  req.query.role === ['guest', 'admin']  (array)

There is no error and no warning. The type just changes silently, and your code either crashes or behaves in ways you did not intend.

The bypass in practice

Here is the pattern that shows up in LLM-generated access control code constantly. It looks reasonable. It even passes a basic code review.

// VULNERABLE: blocklist check assumes req.query.role is always a string
router.get('/dashboard', async (req, res) => {
  const restrictedRoles = ['guest', 'banned'];
  const role = req.query.role;

  if (restrictedRoles.includes(role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  res.json(await getDashboardData());
});

Send GET /dashboard?role=guest&role=moderator.

role is now ['guest', 'moderator']. The check becomes restrictedRoles.includes(['guest', 'moderator']). Array.prototype.includes uses strict equality, and an array object never equals a string. The method returns false. The guest bypasses the blocklist entirely.

This is not theoretical. Blocklist patterns are exactly what LLMs produce when you ask for "restrict access by role" because they match the shape of the prompt. The generated code looks correct on every test that passes a single value for role, which is every test any AI assistant writes.

Beyond blocklist bypasses, there is the crash vector. AI-generated handlers routinely call string methods directly on query parameters without checking the type first:

// Crashes with TypeError when req.query.search is an array
const isMalicious = req.query.search.toLowerCase().includes('drop table');
if (isMalicious) {
  return res.status(400).json({ error: 'Invalid input' });
}

Send ?search=hello&search=DROP TABLE users. req.query.search becomes an array. .toLowerCase() is not a function on arrays. The endpoint throws, returns a 500, and whatever error handler you have either leaks a stack trace or fails silently.

Why vibe-coded APIs are especially exposed

LLMs are trained on code that works in demos. Demo code always sends one value per parameter. The model has seen req.query.id, req.query.role, and req.query.token used as strings in countless examples and generates the same assumptions without questioning them.

The model also does not think adversarially. When you ask it to write authentication middleware, it focuses on the authentication logic. What happens when an attacker deliberately sends a malformed request is not part of the training signal for "write me a route that checks if the user is an admin." That gap is where production incidents live.

There is also a subtler problem with how AI generates middleware stacks. When you ask separately for "authentication middleware" and then "route handler code," the two pieces can apply different assumptions about req.query. One coerces to string. The other does not. The inconsistency is invisible in code review where each piece looks correct in isolation.

AI-generated code also tends to apply schema validation inconsistently. A developer might prompt for Zod validation on request bodies and get it. Query parameters get a manual check written by the LLM in good faith, one that assumes strings throughout.

The fix

Two layers of defense. Both straightforward.

Install hpp and apply it as global middleware. It processes requests before your route handlers see them and replaces duplicate-key arrays with a single value (the last one, by default):

import express from 'express';
import hpp from 'hpp';

const app = express();
app.use(hpp()); // must come before your routes

router.get('/dashboard', async (req, res) => {
  const restrictedRoles = ['guest', 'banned'];
  const role = req.query.role; // now always a string

  if (restrictedRoles.includes(role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  res.json(await getDashboardData());
});

Do not rely solely on middleware. Coerce explicitly at the point where you read query parameters:

const role = Array.isArray(req.query.role)
  ? req.query.role[0]
  : (req.query.role ?? '');

Middleware gets removed. Future contributors do not always know why it was added. Inline coercion documents intent at the site where it matters. If hpp is ever missing or misconfigured, your handler still does the right thing.

Parse query parameters the way you parse any other untrusted input. Because that is what they are.

Seaworthy's static analysis checks for unguarded req.query access in Express route handlers, flagging the patterns described here before they reach production.

Este artículo fue generado por IA y resume fuentes públicamente disponibles.

De los creadores de Seaworthy

Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.

Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.

$ npx seaworthycode