V1 · Codificación y Sanitización#
¿De qué trata esta sección?#
Cuando una aplicación web recibe datos del usuario (un formulario, una URL, una API…), esos datos pueden contener código malicioso. Si la aplicación los procesa sin cuidado, un atacante puede engañarla para que ejecute ese código.
Esta sección define cómo codificar, escapar y sanitizar los datos correctamente para evitar ataques de inyección.
**Concepto clave:** La regla de oro es *nunca confiar en datos externos*. Siempre valida, escapa o sanitiza antes de usar cualquier dato que no generó tu propia aplicación.
V1.1 · Arquitectura de Codificación#
¿Qué pide el estándar?#
| # | Requisito | Nivel |
|---|---|---|
| 1.1.1 | Los datos deben decodificarse una sola vez y antes de cualquier validación o sanitización. | 2 |
| 1.1.2 | La codificación de salida debe hacerse como último paso antes de enviar los datos al intérprete (HTML, SQL, etc.). | 2 |
¿Por qué importa?#
Si decodificas los datos más de una vez, un atacante puede usar doble codificación para evadir tus filtros.
Atacante envía: %2527 (doble codificación de ')
Primera decodificación: %27
Segunda decodificación: ' ← ¡el carácter peligroso se cuela!Ejemplo en código#
# ❌ MAL: decodificando dos veces
import urllib.parse
user_input = "%2527DROP TABLE users%253B"
first_decode = urllib.parse.unquote(user_input) # → %27DROP TABLE users%3B
second_decode = urllib.parse.unquote(first_decode) # → 'DROP TABLE users; ← PELIGROSO
# ✅ BIEN: decodificar una sola vez y luego usar la consulta parametrizada
user_input = "%27usuario"
decoded = urllib.parse.unquote(user_input) # → 'usuario (solo una vez)
# Usar inmediatamente en consulta parametrizada
cursor.execute("SELECT * FROM users WHERE name = ?", (decoded,))V1.2 · Prevención de Inyección#
Esta es una de las secciones más críticas. Cubre los ataques de inyección más comunes: SQL, JavaScript, OS, LDAP, XPath y más.
| # | Requisito | Nivel |
|---|---|---|
| 1.2.1 | Codificar la salida HTTP/HTML según el contexto (elementos, atributos, CSS, headers). | 1 |
| 1.2.2 | Al construir URLs dinámicas, usar URL encoding o base64url. Solo permitir protocolos seguros. | 1 |
| 1.2.3 | Escapar contenido JavaScript/JSON dinámico para evitar inyección. | 1 |
| 1.2.4 | Usar consultas parametrizadas o ORMs para bases de datos (SQL, NoSQL, etc.). | 1 |
| 1.2.5 | Proteger comandos del sistema operativo con queries parametrizados. | 1 |
| 1.2.6 | Proteger contra inyección LDAP. | 2 |
| 1.2.7 | Usar consultas parametrizadas o precompiladas para XPath. | 2 |
| 1.2.8 | Configurar LaTeX de forma segura (sin --shell-escape). | 2 |
| 1.2.9 | Escapar caracteres especiales en expresiones regulares. | 2 |
| 1.2.10 | Proteger contra inyección CSV y de fórmulas. | 3 |
Ejemplos de los ataques más comunes#
💉 SQL Injection#
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
# ❌ MAL: concatenar directamente (vulnerable a SQL Injection)
username = "admin' OR '1'='1"
query = f"SELECT * FROM users WHERE username = '{username}'"
# Genera: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# ¡El atacante accede a TODOS los usuarios!
# ✅ BIEN: consulta parametrizada
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
user = cursor.fetchone()🌐 Cross-Site Scripting (XSS)#
// ❌ MAL: insertar HTML directamente desde input del usuario
const userInput = '<script>document.cookie</script>';
document.getElementById('greeting').innerHTML = `Hola, ${userInput}`;
// El script malicioso se ejecuta en el navegador de la víctima
// ✅ BIEN: usar textContent en lugar de innerHTML
document.getElementById('greeting').textContent = `Hola, ${userInput}`;
// Se muestra como texto plano: Hola, <script>document.cookie</script>
// ✅ BIEN en backend (Python con Flask + Jinja2): auto-escaping activado
// {{ user_input }} → escapa automáticamente los caracteres peligrosos
// {{ user_input | safe }} → ¡NUNCA uses |safe con datos del usuario!
🖥️ OS Command Injection#
import subprocess
# ❌ MAL: ejecutar comandos con input del usuario directamente
filename = "informe.pdf; rm -rf /"
os.system(f"convert {filename} output.png") # ¡Borra el sistema!
# ✅ BIEN: usar lista de argumentos (sin shell=True)
filename = "informe.pdf"
subprocess.run(["convert", filename, "output.png"], shell=False)
# ✅ BIEN: validar con allowlist primero
import re
if not re.match(r'^[a-zA-Z0-9_\-\.]+\.pdf$', filename):
raise ValueError("Nombre de archivo inválido")
subprocess.run(["convert", filename, "output.png"], shell=False)🔗 URL Injection#
// ❌ MAL: construir URLs con datos del usuario sin validar
const redirectUrl = req.query.redirect; // Podría ser: javascript:alert('XSS')
res.redirect(redirectUrl);
// ✅ BIEN: validar contra una lista de protocolos y dominios permitidos
const ALLOWED_DOMAINS = ['miapp.com', 'api.miapp.com'];
function isSafeUrl(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol) &&
ALLOWED_DOMAINS.includes(parsed.hostname);
} catch {
return false;
}
}
const redirectUrl = req.query.redirect;
if (isSafeUrl(redirectUrl)) {
res.redirect(redirectUrl);
} else {
res.redirect('/home'); // fallback seguro
}**Ojo con SQL:** Usar consultas parametrizadas no siempre es suficiente. Los nombres de tablas y columnas (incluyendo ORDER BY) **no pueden parametrizarse**. En esos casos, usa un allowlist de valores permitidos. ```python ALLOWED_COLUMNS = ['nombre', 'fecha', 'precio'] sort_by = request.args.get('sort') if sort_by not in ALLOWED_COLUMNS: sort_by = 'nombre' # valor por defecto seguro query = f"SELECT * FROM productos ORDER BY {sort_by}" ```
V1.3 · Sanitización#
Cuando no puedes evitar que el usuario envíe HTML u otro contenido rico (como en editores WYSIWYG), necesitas sanitizarlo: eliminar o neutralizar el código peligroso.
| # | Requisito | Nivel |
|---|---|---|
| 1.3.1 | Sanitizar HTML de editores WYSIWYG con librerías especializadas y reconocidas. | 1 |
| 1.3.2 | Evitar eval() y ejecución dinámica de código. Si no hay alternativa, sanitizar el input. | 1 |
| 1.3.3 | Sanitizar datos antes de pasarlos a contextos peligrosos. | 2 |
| 1.3.4 | Validar/sanitizar archivos SVG subidos por usuarios. | 2 |
| 1.3.5 | Sanitizar o deshabilitar lenguajes de plantillas (Markdown, CSS, BBCode). | 2 |
| 1.3.6 | Proteger contra SSRF validando protocolos, dominios, rutas y puertos. | 2 |
| 1.3.7 | Proteger contra inyección de plantillas (Template Injection). | 2 |
| 1.3.8 | Sanitizar input en consultas JNDI y configurar JNDI de forma segura. | 2 |
| 1.3.9 | Sanitizar contenido antes de enviarlo a memcache. | 2 |
| 1.3.10 | Sanitizar format strings que puedan resolverse de forma maliciosa. | 2 |
| 1.3.11 | Sanitizar input antes de enviarlo a sistemas de correo (anti SMTP/IMAP injection). | 2 |
| 1.3.12 | Verificar que las expresiones regulares no causen backtracking exponencial (ReDoS). | 3 |
Ejemplo: Sanitización de HTML con DOMPurify#
// ❌ MAL: insertar HTML del usuario directamente
const userHtml = '<p>Hola</p><script>robarContrasena()</script>';
document.getElementById('content').innerHTML = userHtml;
// ✅ BIEN: sanitizar con DOMPurify antes de insertar
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(userHtml);
// Resultado: <p>Hola</p> (el script fue eliminado)
document.getElementById('content').innerHTML = cleanHtml;
// Configuración más estricta: solo permitir ciertas etiquetas
const strictClean = DOMPurify.sanitize(userHtml, {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});Ejemplo: Evitar eval()#
// ❌ MAL: usar eval con input del usuario
const formula = req.body.formula; // podría ser: "process.exit(1)"
const resultado = eval(formula);
// ✅ BIEN: usar un parser matemático seguro
import { evaluate } from 'mathjs';
try {
// mathjs solo evalúa expresiones matemáticas, no código arbitrario
const resultado = evaluate(formula);
} catch (e) {
res.status(400).json({ error: "Fórmula inválida" });
}Ejemplo: Protección contra SSRF#
import ipaddress
import urllib.parse
ALLOWED_DOMAINS = ['api.servicio-externo.com']
def is_safe_url(url: str) -> bool:
try:
parsed = urllib.parse.urlparse(url)
# Solo permitir HTTPS
if parsed.scheme != 'https':
return False
# Solo dominios permitidos
if parsed.netloc not in ALLOWED_DOMAINS:
return False
# Bloquear IPs privadas / localhost
try:
ip = ipaddress.ip_address(parsed.hostname)
if ip.is_private or ip.is_loopback:
return False
except ValueError:
pass # Es un hostname, no una IP → OK
return True
except Exception:
return False
# Uso
url = request.args.get('webhook_url')
if not is_safe_url(url):
abort(400, "URL no permitida")
response = requests.get(url, timeout=5)V1.4 · Memoria, Strings y Código No Administrado#
Aplica principalmente a lenguajes como C, C++ o Assembly donde el desarrollador gestiona la memoria manualmente.
| # | Requisito | Nivel |
|---|---|---|
| 1.4.1 | Usar operaciones seguras de strings/memoria para prevenir buffer overflows. | 2 |
| 1.4.2 | Usar validación de rangos para prevenir integer overflows. | 2 |
| 1.4.3 | Liberar memoria dinámica y anular referencias a memoria liberada (evitar use-after-free). | 2 |
Ejemplo en C (buffer overflow)#
// ❌ MAL: gets() no verifica el tamaño → buffer overflow
char buffer[64];
gets(buffer); // Si el input tiene más de 63 chars, sobreescribe memoria
// ✅ BIEN: fgets() con límite explícito
char buffer[64];
fgets(buffer, sizeof(buffer), stdin); // Nunca lee más de 63 chars + null terminator
// ✅ MEJOR en C++: usar std::string que gestiona memoria automáticamente
#include <string>
std::string input;
std::getline(std::cin, input); // Sin límite fijo, sin overflow
V1.5 · Deserialización Segura#
La deserialización convierte datos almacenados (JSON, XML, binario) en objetos de la aplicación. Si se hace con datos no confiables sin controles, puede permitir ejecución de código arbitrario.
| # | Requisito | Nivel |
|---|---|---|
| 1.5.1 | Configurar parsers XML para deshabilitar entidades externas (prevenir XXE). | 1 |
| 1.5.2 | Usar allowlist de tipos de objeto al deserializar datos no confiables. | 2 |
| 1.5.3 | Usar parsers consistentes para el mismo tipo de dato para evitar vulnerabilidades de interoperabilidad. | 3 |
Ejemplo: XXE (XML External Entity)#
# ❌ MAL: parser XML con entidades externas habilitadas
import xml.etree.ElementTree as ET
# Un atacante podría enviar:
# <?xml version="1.0"?>
# <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
# <data>&xxe;</data>
tree = ET.parse('user_input.xml') # Podría leer archivos del servidor
# ✅ BIEN: usar defusedxml que deshabilita todas las características peligrosas
import defusedxml.ElementTree as SafeET
try:
tree = SafeET.parse('user_input.xml')
except defusedxml.DTDForbidden:
abort(400, "XML con DTD no permitido")Ejemplo: Deserialización insegura#
import pickle, json
# ❌ MAL: pickle con datos del usuario permite ejecución de código arbitrario
data = request.get_data()
obj = pickle.loads(data) # ¡NUNCA hagas esto con datos externos!
# ✅ BIEN: usar JSON con validación de estructura
import json
from pydantic import BaseModel
class UserData(BaseModel):
nombre: str
edad: int
raw = request.get_json()
try:
user = UserData(**raw) # Valida tipos y estructura
except Exception:
abort(400, "Datos inválidos")Resumen Visual#
Datos del usuario
↓
[1. Decodificar UNA sola vez]
↓
[2. Validar formato y tipo] ← V2 lo cubre en detalle
↓
[3. Sanitizar si es necesario]
↓
[4. Usar en contexto correcto]
↓
[5. Codificar/escapar la SALIDA según el intérprete destino]**Recuerda:** La codificación de salida siempre es el **último paso**, justo antes de que los datos lleguen al intérprete (navegador, base de datos, sistema operativo, etc.).