Volver al blog
JavaScriptNode.jsPolución de prototiposSeguridad de objetosOWASP Top 10Seguridad de backendValidación de entradas

Polución de prototipos (Prototype Pollution) en JavaScript y Node.js: guía de prevención para 2026

¿Qué es la polución de prototipos (prototype pollution)?

La polución de prototipos (prototype pollution) es una vulnerabilidad exclusiva del modelo de herencia basado en prototipos de JavaScript. Se produce cuando un atacante manipula __proto__, constructor.prototype u otras propiedades similares en un objeto controlado por el usuario para inyectar propiedades en el Object.prototype global. Una vez contaminado, todos los objetos — incluidos los creados después de la polución — heredan la propiedad inyectada.

js
// A dangerous merge function
function merge(target, source) {
  for (const key of Object.keys(source)) {
    if (source[key] && typeof source[key] === 'object') {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const userInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const config = merge({}, userInput);   // ⚠️ pollutes Object.prototype!
console.log({}.isAdmin);               // → true  (every object is now "admin")

¿La consecuencia? Una sola propiedad contaminada puede eludir comprobaciones de autorización, desactivar funciones de seguridad o tumbar todo tu servidor — todo a partir de un payload JSON que controla el atacante.

Por qué las startups deberían preocuparse

La polución de prototipos no es una vulnerabilidad de nicho. Ha aparecido en:

  • lodash (CVE-2020-8203, CVE-2019-10744 — polución de prototipos vía _.merge, _.defaultsDeep)
  • Body-parsers de Express (al procesar objetos JSON anidados sin límites de profundidad)
  • Socket.io y otras librerías en tiempo real con gestión de configuración deep-merge
  • jQuery (CVE-2019-11358 — $.extend(true, ...))
  • Drivers de Mongoose / MongoDB (expansión automática de objetos a partir de parámetros de consulta)

Para una startup que lanza rápido, la combinación de utilidades de deep-merge, entradas JSON sin sanitizar y procesamiento recursivo de objetos crea una tormenta perfecta. He visto un bug de polución de prototipos escalar de "parseo JSON inofensivo" a "compromiso total de la aplicación" en producción a las pocas horas del despliegue.

Cómo la explotan los atacantes

1. Eludiendo comprobaciones de autorización

js
// Backend code checking admin status
function isAdmin(user) {
  return user.role === 'admin';
}

// Attacker sends: {"__proto__": {"role": "admin"}}
// After a recursive merge, every object has role === 'admin'
// isAdmin({}) → true

2. Denegación de servicio vía __defineGetter__

js
// Older JavaScript engines allowed getter injection
const payload = '{"__proto__": {"toString": {"__defineGetter__": "..."}}';
// Can hang the process when any object is coerced to string

3. Sombreado de propiedades en renderizado del lado del servidor

js
// Next.js server component receives polluted query params
// Object.prototype now has .dangerous = "value",
// which shadows expected properties in component props

4. Eludiendo validadores de lista blanca

js
// A schema validator checks allowed properties
const allowed = ['name', 'email', 'role'];
function sanitize(obj) {
  const clean = {};
  for (const key of allowed) {
    if (key in obj) clean[key] = obj[key];
  }
  return clean;
}

// After prototype pollution, 'isAdmin' is always "in" obj
// because it's inherited via prototype, even though it's not own
'isAdmin' in {}  // → true after pollution

Patrones de prevención (con código)

1. Usa Object.create(null) para diccionarios

La defensa más efectiva con diferencia. Los objetos creados con prototipo null no tienen cadena de prototipos — ni __proto__, ni constructor.prototype, ni toString.

js
// ✅ Immune to prototype pollution
const config = Object.create(null);

// ✅ Safe for caches, lookup tables, user-data maps
const userCache = Object.create(null);
userCache[sessionId] = userData;

// ✅ Safe for parsed query parameters
const params = Object.create(null);
params[key] = value;

Cuándo usarlo: Cualquier objeto que sirva como mapa, diccionario, tabla de búsqueda o que contenga claves controladas por el usuario. Es decir, la mayor parte del parseo JSON, el parseo de query strings y el manejo del cuerpo de peticiones.

2. Elimina las claves de prototipo antes de fusionar

js
// ✅ Safe merge with explicit prototype key exclusion
function safeMerge(target, ...sources) {
  const PROTOTYPE_KEYS = new Set([
    '__proto__',
    'constructor',
    'prototype',
  ]);

  for (const source of sources) {
    for (const key of Object.keys(source)) {
      if (PROTOTYPE_KEYS.has(key)) continue;     // 🛡️ skip dangerous keys
      if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
      // ^ also skip inherited properties that might be from a previous pollution

      const val = source[key];
      if (val && typeof val === 'object' && !Array.isArray(val)) {
        target[key] = safeMerge(
          Object.prototype.hasOwnProperty.call(target, key)
            ? target[key]
            : Object.create(null),
          val
        );
      } else {
        target[key] = val;
      }
    }
  }
  return target;
}

3. Usa hasOwnProperty en lugar de in

js
// ❌ DANGEROUS — 'key in obj' checks the prototype chain
if ('isAdmin' in userData) { /* may be true even without own property */ }

// ✅ SAFE — only checks own properties
if (Object.prototype.hasOwnProperty.call(userData, 'isAdmin')) {
  // truly own
}

// ✅ or with Object.hasOwn() (ES2022+)
if (Object.hasOwn(userData, 'isAdmin')) {
  // truly own
}

4. Usa Map en lugar de objetos planos

Para el almacenamiento clave-valor, Map es inherentemente inmune a la polución de prototipos — las claves son entradas reales, no propiedades:

js
// ✅ Map is safe — keys are entries, not properties
const cache = new Map();
cache.set(userInputKey, value);
// No __proto__ attack possible

// ✅ Safer for rate-limiting buckets, session stores, memoization

5. Aplica límites de profundidad en el procesamiento recursivo

js
// ✅ Limit recursion depth to prevent prototype chain traversal
function safeDeepClone(obj, maxDepth = 5) {
  const seen = new WeakMap();

  function clone(value, depth) {
    if (depth > maxDepth) return Object.create(null);   // 🛡️ cap depth
    if (value === null || typeof value !== 'object') return value;

    if (seen.has(value)) return seen.get(value);

    const cloneTarget = Array.isArray(value) ? [] : Object.create(null);
    seen.set(value, cloneTarget);

    for (const key of Object.keys(value)) {
      if (key === '__proto__' || key === 'constructor') continue;  // 🛡️
      if (Object.prototype.hasOwnProperty.call(value, key)) {
        cloneTarget[key] = clone(value[key], depth + 1);
      }
    }
    return cloneTarget;
  }

  return clone(value, 0);
}

6. Valida los esquemas de entrada antes de cualquier procesamiento

js
import { z } from 'zod';
// or: ajv, yup, joi

const UserSchema = z.object({
  name: z.string().max(100),
  email: z.string().email(),
  role: z.enum(['user', 'admin']).optional(),
  // ⚠️ No __proto__, constructor, or prototype fields allowed
}).strict();   // 🛡️ .strict() rejects unknown keys

app.post('/api/user', (req, res) => {
  // Parse BEFORE any merge or spread — rejection if __proto__ present
  const result = UserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ error: 'Invalid payload' });
  }
  // Now it's safe to use the parsed data
});

Protecciones específicas de Next.js

Parseo seguro de query strings

js
// apps/my-app/src/lib/query-parser.ts

// ❌ DANGEROUS — Object.assign or spread inherits prototype
// const params = { ...req.query };

// ✅ SAFE — extract only expected keys
export function parseSafeQuery(query: Record<string, string>, allowed: string[]) {
  const params: Record<string, string> = Object.create(null);
  for (const key of allowed) {
    if (typeof query[key] === 'string') {
      params[key] = query[key];
    }
  }
  return params;
}

Validación de entradas en Server Actions

js
// apps/my-app/src/actions/user.ts
'use server';

export async function updateProfile(formData: FormData) {
  // FormData entries are already safe (they don't carry __proto__),
  // but if you convert to JSON, be careful
  const raw = Object.fromEntries(formData.entries());  // ⚡ safe — entries are strings
  const data = ProfileSchema.parse(raw);               // 🛡️ validate!
}

Protección de rutas API con middleware

js
// apps/my-app/src/middleware.ts
export function middleware(request: NextRequest) {
  const contentType = request.headers.get('content-type');
  if (contentType?.includes('application/json')) {
    // Next.js will parse the JSON body — ensure depth limits
    // by configuring bodyParser in next.config or using a custom parser
  }
}

Configura los límites de profundidad en next.config.ts:

ts
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  experimental: {
    serverComponentsBodySizeLimit: '1mb',
  },
  // Body parser depth limit for API routes
  // (configure via custom middleware if needed)
};

Lista de verificación de auditoría

Úsala en tu próxima revisión de código o auditoría de seguridad:

| Comprobación | Qué buscar | |---|---| | Merge/clonado recursivo | _.merge, _.defaultsDeep, $.extend(true) o merges recursivos personalizados — deben eliminar __proto__/constructor | | Spread de objetos sobre datos de usuario | { ...req.body }, Object.assign({}, userInput) — usa un destino Object.create(null) o parsea con esquema primero | | Comprobaciones con el operador in | Sustitúyelas por Object.hasOwn() o Object.prototype.hasOwnProperty.call() | | Objetos planos como mapas | Migra a Map, WeakMap o Object.create(null) | | Profundidad del parseo JSON | Asegúrate de que JSON.parse no recorra más de 5–10 niveles | | Validación de esquemas | Todas las entradas de usuario deberían pasar un parser de esquemas estricto (Zod, Ajv, Yup) antes de cualquier operación con objetos | | Profundidad de express.json() | El parser JSON por defecto de Express no tiene límite de profundidad — configura { limit: '1mb', strict: true } | | Rutas API de Next.js | Si usas un middleware de parseo JSON personalizado, asegúrate de que elimina las claves de prototipo |

Soporte de herramientas

  • Regla de Semgrep: js/express-prototype-pollution — detecta patrones de merge inseguros en CI
  • eslint-plugin-security: La regla no-prototype-builtins detecta comprobaciones hasOwnProperty ausentes
  • Socket.dev: Detección en tiempo de ejecución de polución de prototipos en tu árbol de dependencias
  • --frozen-intrinsics de Node.js (experimental): Congela Object.prototype, Array.prototype y otros prototipos integrados para prevenir cualquier polución en tiempo de ejecución
bash
# Run Node with frozen intrinsics to prevent prototype mutation
node --frozen-intrinsics server.js

Resumen

La polución de prototipos es una vulnerabilidad nativa de JavaScript que no aparece en la mayoría de los escáneres SAST y es invisible para las bases de datos de vulnerabilidades tradicionales. Además, es trivialmente fácil de introducir — un merge defectuoso, un clonado recursivo, una clave __proto__ sin comprobar.

La corrección no es complicada:

  1. Object.create(null) para cualquier contenedor de diccionarios o datos de usuario
  2. Elimina las claves de prototipo (__proto__, constructor, prototype) en cada operación recursiva
  3. Map para el almacenamiento clave-valor
  4. Validación de esquemas antes de cualquier manipulación de objetos
  5. Límites de profundidad en el procesamiento recursivo

Aplica estos cinco patrones de forma consistente y eliminarás toda una clase de vulnerabilidades que la mayoría de las bases de código JavaScript dejan completamente abiertas.

Comparte este artículo:TwitterLinkedIn
JS

JS Security Audit

Auditorías dirigidas por un ingeniero de seguridad JavaScript senior con más de 10 años de experiencia.