Saltar al contenido principal

Fundamentos de codificación segura

La mayoría de las vulnerabilidades de seguridad en aplicaciones web provienen de un pequeño conjunto de errores comunes. Inyección SQL, cross-site scripting, autenticación rota — estas son clases de vulnerabilidades bien documentadas, sin embargo siguen apareciendo en código nuevo porque los desarrolladores no aprenden a evitarlas.

La buena noticia: no necesita convertirse en un experto en seguridad para evitar estos errores. Entender las 10 clases de vulnerabilidades más comunes y aplicar patrones consistentes para prevenirlas eliminará la gran mayoría de los problemas de seguridad en su código.

Este capítulo cubre el OWASP Top 10, técnicas prácticas de prevención y una lista de verificación que su equipo puede usar durante la revisión de código.

El OWASP Top 10

OWASP (Open Web Application Security Project) mantiene una lista de los diez riesgos de seguridad más críticos en aplicaciones web. La lista se actualiza cada pocos años en función de datos del mundo real obtenidos de evaluaciones de seguridad.

Esta es la lista actual (2021) con contexto práctico para equipos pequeños:

A01: Control de acceso roto

Los usuarios pueden acceder a datos o funciones que no deberían. La clase de vulnerabilidad más común.

Cómo se ve:

# BAD: Anyone can view any user's profile by changing the ID
@app.route('/api/users/<user_id>/profile')
def get_profile(user_id):
return User.query.get(user_id).to_dict()

La corrección:

# GOOD: Check that the requesting user can access this profile
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
if current_user.id != int(user_id) and not current_user.is_admin:
abort(403)
return User.query.get(user_id).to_dict()

Patrones de prevención:

  • Denegar por defecto. Conceda acceso explícitamente, no lo deniegue explícitamente.
  • Verifique la autorización en cada solicitud, no solo en la UI.
  • Use referencias indirectas (UUIDs) en lugar de IDs secuenciales.
  • Registre los fallos de control de acceso.

A02: Fallos criptográficos

Datos sensibles expuestos por cifrado débil o ausente.

Errores comunes:

  • Almacenar contraseñas en texto plano o con hashing débil (MD5, SHA1)
  • Transmitir datos por HTTP en lugar de HTTPS
  • Codificar claves de cifrado en el código fuente
  • Usar algoritmos criptográficos obsoletos

Qué hacer:

# BAD: MD5 for passwords
password_hash = hashlib.md5(password.encode()).hexdigest()

# GOOD: bcrypt with proper cost factor
from bcrypt import hashpw, gensalt, checkpw
password_hash = hashpw(password.encode(), gensalt(rounds=12))

Patrones de prevención:

  • Use bcrypt, Argon2 o scrypt para el hashing de contraseñas. Nunca MD5 o SHA1.
  • Aplique HTTPS en todos lados. Redirija HTTP a HTTPS.
  • Almacene secretos en variables de entorno o un gestor de secretos (Passwork, HashiCorp Vault), nunca en el código.
  • Use TLS 1.2 o superior para los datos en tránsito.

A03: Inyección

Datos no confiables enviados a un intérprete como parte de un comando o consulta. La inyección SQL es el ejemplo clásico, pero esto también cubre NoSQL, comandos del SO, LDAP y otros.

Ejemplo de inyección SQL:

# BAD: String concatenation with user input
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)

# GOOD: Parameterized query
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))

Ejemplo de inyección de comandos:

# BAD: User input in shell command
os.system(f"ping {host}")

# GOOD: Use subprocess with argument list
subprocess.run(["ping", "-c", "4", host], capture_output=True)

Patrones de prevención:

  • Use consultas parametrizadas (sentencias preparadas) para todo el acceso a la base de datos.
  • Use ORMs que manejen la parametrización automáticamente.
  • Valide y sanitice todas las entradas.
  • Nunca pase entradas del usuario directamente a comandos del sistema.

A04: Diseño inseguro

Problemas de seguridad integrados en la arquitectura, no solo errores de implementación.

Ejemplos:

  • Recuperación de contraseña que envía la contraseña real por correo (significa que la almacena de forma recuperable)
  • "Preguntas de seguridad" que son fáciles de investigar (nombre de soltera de la madre, primera mascota)
  • Sin rate limiting en los endpoints de autenticación
  • Permitir cargas de archivos ilimitadas sin restricciones de tamaño o tipo

Patrones de prevención:

  • Modele amenazas durante el diseño, no después.
  • Considere los casos de abuso: "¿Qué pasa si alguien intenta romper esto?"
  • Aplique defensa en profundidad — múltiples capas de protección.
  • Siga patrones de diseño de seguridad establecidos.

A05: Configuración de seguridad incorrecta

Credenciales por defecto, funciones innecesarias habilitadas, mensajes de error detallados, cabeceras de seguridad ausentes.

Problemas comunes:

  • Contraseñas de administrador por defecto no cambiadas
  • Modo de depuración habilitado en producción
  • Listado de directorios habilitado
  • Cabeceras de seguridad ausentes (CSP, X-Frame-Options)
  • Servicios innecesarios en ejecución

Patrones de prevención:

  • Automatice la configuración con Infrastructure as Code.
  • Elimine funciones y frameworks no utilizados.
  • Use verificadores de cabeceras de seguridad (securityheaders.com).
  • Implemente configuraciones diferentes para dev/staging/producción.

A06: Componentes vulnerables y desactualizados

Uso de librerías o frameworks con vulnerabilidades conocidas.

El problema: Su aplicación puede ser segura, pero si usa una librería con un CVE publicado, los atacantes tienen un exploit listo.

Patrones de prevención:

  • Mantenga las dependencias actualizadas.
  • Use Dependabot, Snyk o npm audit para monitorear vulnerabilidades.
  • Elimine las dependencias no utilizadas.
  • Suscríbase a los avisos de seguridad para su stack.

Este tema se trata en profundidad en el capítulo de gestión de vulnerabilidades.

A07: Fallos de identificación y autenticación

Contraseñas débiles, credential stuffing, errores de gestión de sesiones.

Problemas comunes:

  • Sin protección contra fuerza bruta
  • Tokens de sesión en URLs
  • Sesiones que no caducan
  • Tokens de restablecimiento de contraseña que no caducan

Patrones de prevención:

  • Implemente MFA (cubierto anteriormente en este curso).
  • Use la gestión de sesiones establecida de su framework.
  • Aplique requisitos de complejidad de contraseña.
  • Limite la velocidad de los intentos de autenticación.
  • Invalide las sesiones al cerrar sesión y al cambiar la contraseña.

A08: Fallos en la integridad de software y datos

Código o datos modificados sin verificación. Incluye deserialización insegura y ataques al pipeline CI/CD.

Ejemplos:

  • Deserializar datos no confiables (pickle, serialización de Java)
  • Cargar JavaScript desde CDNs no confiables sin verificaciones de integridad
  • Pipelines CI/CD que ejecutan código arbitrario de pull requests

Patrones de prevención:

  • Use Subresource Integrity (SRI) para scripts externos.
  • Evite deserializar datos no confiables. Si es necesario, use alternativas seguras (JSON en lugar de pickle).
  • Firme y verifique la integridad del código y los datos.
  • Asegure su pipeline CI/CD.

A09: Fallos en el registro y monitoreo de seguridad

No puede detectar ni responder a los ataques si no está registrando.

Qué registrar:

  • Éxitos y fallos de autenticación
  • Fallos de autorización
  • Fallos de validación de entrada
  • Errores de la aplicación

Qué no registrar:

  • Contraseñas o tokens de sesión
  • Números de tarjetas de crédito
  • Datos personales más allá de lo necesario

Patrones de prevención:

  • Implemente registro centralizado.
  • Configure alertas para patrones sospechosos.
  • Incluya suficiente contexto para investigar (marca de tiempo, usuario, IP, acción).
  • Proteja los registros contra manipulaciones.

A10: Server-Side Request Forgery (SSRF)

La aplicación obtiene URLs proporcionadas por los usuarios sin validación, permitiendo a los atacantes acceder a recursos internos.

Ejemplo:

# BAD: Fetching user-provided URL
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
return requests.get(url).text # Can access internal services!

Patrones de prevención:

  • Valide y sanitice la entrada de URL.
  • Use listas de permitidos para los dominios permitidos.
  • Bloquee solicitudes a rangos de IP internos (10.x, 172.16.x, 192.168.x, 169.254.x).
  • Use segmentación de red para que el servidor web no pueda acceder a los servicios internos.

Validación de entrada

La mayoría de las vulnerabilidades implican que una entrada no confiable se procesa de forma insegura. La validación de entrada es su primera línea de defensa.

Principios de validación

Valide en el servidor, no solo en el cliente. La validación del lado del cliente mejora la UX pero no proporciona seguridad. Cualquiera puede omitir la validación de JavaScript enviando solicitudes directamente.

Valide contra una lista de permitidos cuando sea posible. Defina qué es aceptable, rechace todo lo demás.

# BAD: Blocklist approach (trying to block bad things)
if "<script>" in input:
reject()

# GOOD: Allowlist approach (only accept known good patterns)
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
reject()

Valide tipo, longitud, formato y rango.

def validate_user_input(data):
errors = []

# Type validation
if not isinstance(data.get('age'), int):
errors.append("Age must be an integer")

# Range validation
if data.get('age', 0) < 0 or data.get('age', 0) > 150:
errors.append("Age must be between 0 and 150")

# Length validation
if len(data.get('username', '')) > 50:
errors.append("Username too long")

# Format validation
if not re.match(r'^[a-zA-Z0-9_]+$', data.get('username', '')):
errors.append("Username contains invalid characters")

return errors

Patrones de validación comunes

Direcciones de correo electrónico:

import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
raise ValidationError("Invalid email format")

URLs:

from urllib.parse import urlparse

def validate_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValidationError("Invalid URL scheme")
if not parsed.netloc:
raise ValidationError("Invalid URL")

Carga de archivos:

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB

def validate_file(file):
# Check extension
ext = file.filename.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValidationError(f"File type not allowed: {ext}")

# Check size
file.seek(0, 2) # Seek to end
size = file.tell()
file.seek(0) # Reset
if size > MAX_FILE_SIZE:
raise ValidationError("File too large")

# Check magic bytes (file signature)
# Don't trust the extension alone

Codificación de salida

La validación evita que los datos malos entren en su sistema. La codificación de salida evita que los datos almacenados se ejecuten en un contexto dañino.

Codificación específica del contexto

Contexto HTML:

# BAD: Raw output in HTML
return f"<p>Welcome, {username}</p>"

# GOOD: HTML-encode the output
from markupsafe import escape
return f"<p>Welcome, {escape(username)}</p>"

# Or use a template engine that auto-escapes (Jinja2, Django templates)

Contexto JavaScript:

# BAD: User data in JavaScript
return f'<script>var name = "{username}";</script>'

# GOOD: JSON-encode for JavaScript context
import json
return f'<script>var name = {json.dumps(username)};</script>'

Contexto URL:

# BAD: Raw value in URL
return f'<a href="/search?q={query}">Search</a>'

# GOOD: URL-encode
from urllib.parse import quote
return f'<a href="/search?q={quote(query)}">Search</a>'

Contexto SQL: Use siempre consultas parametrizadas. Ninguna cantidad de codificación sustituye a la parametrización adecuada de consultas.

Protección CSRF

Cross-Site Request Forgery engaña al navegador del usuario para que realice solicitudes no deseadas a un sitio donde está autenticado. Si ha iniciado sesión en su banco, un sitio malicioso podría enviar una solicitud de transferencia en su nombre.

Cómo funciona CSRF

1. User logs into bank.com, gets session cookie
2. User visits malicious-site.com (in another tab)
3. Malicious site contains: <img src="https://bank.com/transfer?to=attacker&amount=1000">
4. Browser sends request to bank.com WITH the session cookie
5. Bank processes the transfer — user is authenticated

Tokens CSRF

La defensa estándar: incluir un token secreto en los formularios que el atacante no pueda adivinar.

El servidor genera el token:

import secrets

def get_csrf_token(session):
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return session['csrf_token']

Incluir en los formularios:

<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- other fields -->
</form>

Validar al enviar:

@app.route('/transfer', methods=['POST'])
def transfer():
if request.form.get('csrf_token') != session.get('csrf_token'):
abort(403, 'Invalid CSRF token')
# Process the request

Soporte de frameworks:

La mayoría de los frameworks gestionan CSRF automáticamente:

# Django - enabled by default
# Just include {% csrf_token %} in forms

# Flask-WTF
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
// Express.js with csurf
const csrf = require('csurf');
app.use(csrf({ cookie: true }));

app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});

Cookies SameSite

Los navegadores modernos admiten el atributo de cookie SameSite, que evita que las cookies se envíen con solicitudes de sitios cruzados.

# Set SameSite attribute
response.set_cookie(
'session',
value=session_id,
httponly=True,
secure=True,
samesite='Lax' # or 'Strict'
)

Valores de SameSite:

  • Strict — La cookie nunca se envía con solicitudes de sitios cruzados. La más segura, pero rompe los enlaces legítimos de otros sitios.
  • Lax — La cookie se envía con la navegación de nivel superior (clic en enlaces) pero no con solicitudes incrustadas (imágenes, iframes, AJAX). Buen valor por defecto.
  • None — La cookie se envía con todas las solicitudes. Debe usar el indicador Secure. Solo para casos de uso legítimos de sitios cruzados.

Recomendación: Use SameSite=Lax más tokens CSRF para defensa en profundidad. No confíe solo en SameSite — los navegadores más antiguos no lo admiten.

CSRF para APIs

Para APIs que usan tokens (no cookies), CSRF no es un problema — el atacante no puede robar el token de otro sitio.

Pero si su API usa autenticación basada en cookies:

  • Requiera una cabecera personalizada (p.ej., X-Requested-With) — los navegadores no envían cabeceras personalizadas en solicitudes simples de origen cruzado
  • O requiera el token CSRF en una cabecera
// Frontend sends CSRF token in header
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});

Seguridad de API

Si está construyendo APIs REST, hay consideraciones de seguridad adicionales.

Rate limiting

Sin rate limiting, los atacantes pueden:

  • Aplicar fuerza bruta a las contraseñas
  • Raspar sus datos
  • Realizar ataques DoS a su servicio
  • Aumentar sus costos de infraestructura

Express.js:

const rateLimit = require('express-rate-limit');

// General rate limit
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later'
});
app.use('/api/', limiter);

// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
message: 'Too many login attempts'
});
app.use('/api/login', authLimiter);

Flask:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per hour")
def login():
# ...

@app.route('/api/data')
@limiter.limit("100 per minute")
def get_data():
# ...

Django:

# Using django-ratelimit
from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='5/h', method='POST', block=True)
def login(request):
# ...

Estrategias de rate limiting:

  • Por dirección IP (puede ser evadido con proxies)
  • Por cuenta de usuario (para endpoints autenticados)
  • Por clave API
  • Combinación de las anteriores

Configuración de CORS

CORS (Cross-Origin Resource Sharing) controla qué sitios web pueden llamar a su API desde JavaScript.

El problema:

// From evil-site.com
fetch('https://your-api.com/user/data')
.then(r => r.json())
.then(data => sendToAttacker(data));

Sin cabeceras CORS, los navegadores bloquean esto. Con CORS mal configurado, los atacantes pueden robar datos de usuarios autenticados.

Configuración incorrecta:

# DON'T DO THIS - allows any site to access your API
@app.after_request
def add_cors(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Credentials'] = 'true' # DANGEROUS with *
return response

Buena configuración:

# Flask-CORS
from flask_cors import CORS

# Allow only your frontend
CORS(app, origins=['https://your-frontend.com'], supports_credentials=True)
// Express.js
const cors = require('cors');

app.use(cors({
origin: 'https://your-frontend.com',
credentials: true
}));

Reglas de CORS:

  • Nunca use Access-Control-Allow-Origin: * con Access-Control-Allow-Credentials: true
  • Lista blanca de orígenes específicos, no refleje la cabecera Origin ciegamente
  • Sea restrictivo con los métodos y cabeceras permitidos

Autenticación de API

Claves API:

@app.before_request
def check_api_key():
if request.path.startswith('/api/'):
api_key = request.headers.get('X-API-Key')
if not api_key or not validate_api_key(api_key):
abort(401)

Buenas prácticas para claves API:

  • Genere claves largas y aleatorias (32+ caracteres)
  • Almacene con hash en la base de datos (como contraseñas)
  • Permita la rotación de claves sin tiempo de inactividad
  • Registre el uso de claves para auditoría
  • Implemente ámbitos/permisos de clave

Seguridad JWT

Los JSON Web Tokens se usan comúnmente para la autenticación de API. También se configuran mal con frecuencia.

Cómo funciona JWT

Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjM0fQ.signature

El servidor firma el token con una clave secreta. El cliente lo envía de vuelta con las solicitudes. El servidor verifica la firma para confiar en el payload.

Errores comunes de JWT

Error 1: Confusión de algoritmo (alg:none)

Algunas librerías JWT aceptan alg: "none", lo que significa que no hay verificación de firma.

# Attacker crafts token with no signature
# Header: {"alg": "none"}
# Payload: {"user_id": "admin"}
# Signature: (empty)

Corrección: Valide siempre el algoritmo explícitamente:

import jwt

# BAD: Library might accept "none"
data = jwt.decode(token, SECRET_KEY)

# GOOD: Explicitly specify allowed algorithms
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])

Error 2: Secretos débiles o filtrados

# BAD
SECRET_KEY = 'secret' # Easily guessable

# GOOD
SECRET_KEY = os.environ.get('JWT_SECRET') # At least 256 bits of entropy

Error 3: Almacenar en localStorage

// BAD: XSS can steal the token
localStorage.setItem('token', jwt);

// BETTER: HttpOnly cookie (not accessible to JavaScript)
// Set from server with HttpOnly flag

Si debe usar localStorage, implemente protecciones adicionales:

  • Expiración corta del token
  • Rotación de tokens
  • Fingerprinting

Error 4: Sin expiración

# BAD: Token valid forever
payload = {'user_id': user.id}
token = jwt.encode(payload, SECRET_KEY)

# GOOD: Short expiration
from datetime import datetime, timedelta

payload = {
'user_id': user.id,
'exp': datetime.utcnow() + timedelta(hours=1)
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

Error 5: Datos sensibles en el payload

Los JWTs están codificados, no cifrados. Cualquiera puede decodificar el payload.

# BAD: Don't put secrets in JWT
payload = {
'user_id': 123,
'credit_card': '4111111111111111' # Visible to anyone!
}

# GOOD: Only include non-sensitive identifiers
payload = {
'user_id': 123,
'roles': ['user']
}

Buenas prácticas de JWT

import jwt
from datetime import datetime, timedelta

def create_token(user_id, roles):
payload = {
'sub': user_id, # Subject
'roles': roles,
'iat': datetime.utcnow(), # Issued at
'exp': datetime.utcnow() + timedelta(hours=1), # Expiration
'jti': secrets.token_hex(16) # Unique token ID (for revocation)
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def verify_token(token):
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=['HS256'], # Explicit algorithm
options={'require': ['exp', 'sub']} # Required claims
)
return payload
except jwt.ExpiredSignatureError:
raise AuthError('Token expired')
except jwt.InvalidTokenError:
raise AuthError('Invalid token')

Comparación de almacenamiento de tokens

AlmacenamientoVulnerable a XSSVulnerable a CSRFRecomendación
localStorageNoEvitar para tokens sensibles
sessionStorageNoLigeramente mejor (se borra al cerrar la pestaña)
Cookie HttpOnlyNoSí (mitigar con SameSite)Mejor para aplicaciones web
Solo en memoriaNoNoMejor seguridad, pero se pierde al recargar

Para la mayoría de las aplicaciones web: Use cookies HttpOnly con SameSite=Lax.

Para SPAs que necesitan acceso al token: Use tokens de acceso de corta duración en memoria + tokens de actualización HttpOnly en cookies.

Manejo seguro de secretos

Los secretos en el código fuente son uno de los problemas de seguridad más comunes en las empresas pequeñas.

Qué cuenta como un secreto

  • Claves API
  • Contraseñas de bases de datos
  • Claves de cifrado
  • Secretos de cliente OAuth
  • Claves de firma JWT
  • Credenciales de cuentas de servicio
  • Claves privadas SSH

Dónde NO deben estar los secretos

  • Archivos de código fuente
  • Repositorios Git (incluso si se eliminan después — el historial de Git los conserva)
  • Archivos de configuración confirmados en el control de versiones
  • Archivos de registro
  • Mensajes de error
  • Código del lado del cliente (JavaScript, aplicaciones móviles)

Dónde SÍ deben estar los secretos

Variables de entorno:

# config.py
import os

DATABASE_URL = os.environ.get('DATABASE_URL')
API_KEY = os.environ.get('API_KEY')

Gestor de secretos (recomendado):

# Using Passwork CLI or API
import subprocess
import json

def get_secret(secret_id):
result = subprocess.run(
['passwork-cli', 'get', secret_id, '--format', 'json'],
capture_output=True
)
return json.loads(result.stdout)['password']

DATABASE_PASSWORD = get_secret('database-production')

Gestores de secretos en la nube:

# AWS Secrets Manager
import boto3

def get_secret(secret_name):
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId=secret_name)
return response['SecretString']

Comprobación de secretos en el código

Use herramientas automatizadas para escanear secretos confirmados accidentalmente:

# Install git-secrets
brew install git-secrets

# Add common patterns
git secrets --register-aws

# Scan repository
git secrets --scan

# Install as pre-commit hook
git secrets --install

Otras herramientas:

  • truffleHog — Escanea el historial de Git en busca de cadenas de alta entropía
  • gitleaks — Escaneo rápido de secretos en repositorios Git
  • detect-secrets — Herramienta de Yelp para prevenir secretos en el código

Vulnerabilidades comunes por lenguaje

JavaScript/Node.js

Contaminación de prototipos:

// BAD: Merging user input into objects
Object.assign(config, userInput);

// GOOD: Create new object, validate keys
const sanitized = {};
for (const key of allowedKeys) {
if (key in userInput) {
sanitized[key] = userInput[key];
}
}

eval() y Function():

// BAD: Never use eval with user input
eval(userInput);

// GOOD: Use JSON.parse for data, avoid eval entirely
const data = JSON.parse(userInput);

Inyección NoSQL (MongoDB):

// BAD: Query from user input
db.users.find({ username: req.body.username });
// Attacker sends: { "$gt": "" } to match all users

// GOOD: Explicitly expect a string
const username = String(req.body.username);
db.users.find({ username: username });

Python

Deserialización insegura:

# BAD: pickle with untrusted data
import pickle
data = pickle.loads(user_input) # Can execute arbitrary code!

# GOOD: Use JSON
import json
data = json.loads(user_input)

Inyección de plantillas:

# BAD: User input in template string
from jinja2 import Template
Template(user_input).render() # SSTI vulnerability

# GOOD: User input as template variable
Template("Hello {{ name }}").render(name=user_input)

PHP

Inclusión de archivos locales:

// BAD: User input in include
include($_GET['page'] . '.php');

// GOOD: Allowlist of permitted pages
$allowed = ['home', 'about', 'contact'];
if (in_array($_GET['page'], $allowed)) {
include($_GET['page'] . '.php');
}

Comparación de tipos:

// BAD: Loose comparison with user input
if ($password == $user_password) { ... }

// GOOD: Strict comparison
if ($password === $user_password) { ... }

// BEST: Use password_verify for passwords
if (password_verify($password, $hash)) { ... }

Java

XML External Entity (XXE):

// BAD: Default XML parser settings
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = db.parse(userInput);

// GOOD: Disable external entities
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder db = dbf.newDocumentBuilder();

Deserialización insegura:

// BAD: Deserializing untrusted data
ObjectInputStream ois = new ObjectInputStream(userInputStream);
Object obj = ois.readObject(); // Can execute arbitrary code

// GOOD: Use JSON or validate object types

Cabeceras de seguridad

Configure su servidor web o aplicación para enviar cabeceras de seguridad:

# Content Security Policy - controls what resources can load
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'

# Prevent clickjacking
X-Frame-Options: DENY

# Prevent MIME type sniffing
X-Content-Type-Options: nosniff

# Enable browser XSS filter
X-XSS-Protection: 1; mode=block

# Force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin

# Prevent browser features you don't use
Permissions-Policy: geolocation=(), microphone=(), camera=()

Ejemplo con Express.js:

const helmet = require('helmet');
app.use(helmet());

Ejemplo con Django:

# settings.py
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000

Lista de verificación de codificación segura

Use esto durante la revisión de código:

Autenticación y autorización

  • Contraseñas hasheadas con bcrypt/Argon2 (no MD5/SHA1)
  • Los tokens de sesión son aleatorios, largos y HTTP-only
  • La autorización se verifica en cada solicitud
  • Los intentos fallidos de inicio de sesión tienen rate limiting
  • Los tokens de restablecimiento de contraseña caducan

Entrada y salida

  • Toda la entrada se valida en el servidor
  • La validación usa listas de permitidos donde sea posible
  • SQL usa consultas parametrizadas
  • La salida HTML está codificada
  • Las cargas de archivos están validadas (tipo, tamaño, contenido)

Secretos

  • Sin secretos en el código fuente
  • Los secretos se cargan desde el entorno o un gestor de secretos
  • Las claves API tienen los permisos mínimos necesarios
  • Los secretos son rotables sin despliegue de código

Configuración

  • Modo de depuración deshabilitado en producción
  • Los mensajes de error no exponen los internos
  • Las cabeceras de seguridad configuradas
  • HTTPS aplicado

Dependencias

  • Dependencias actualizadas
  • Sin versiones vulnerables conocidas
  • Dependencias no utilizadas eliminadas

Taller: asegure su código base

Reserve 2-3 horas para esta evaluación y las correcciones iniciales.

Parte 1: escaneo de secretos (30 minutos)

  1. Instale un escáner de secretos:
pip install trufflehog
# or
brew install gitleaks
  1. Escanee su repositorio:
trufflehog git file://. --only-verified
# or
gitleaks detect --source .
  1. Documente los hallazgos
  2. Rote los secretos expuestos inmediatamente
  3. Elimine los secretos del código y del historial de Git si es necesario

Entregable: Lista de secretos encontrados y estado de remediación

Parte 2: auditoría de dependencias (30 minutos)

  1. Ejecute la herramienta de auditoría de su lenguaje:
# Node.js
npm audit

# Python
pip-audit

# Ruby
bundle audit

# PHP
composer audit
  1. Documente los paquetes vulnerables
  2. Actualice lo que pueda inmediatamente
  3. Cree tickets para las actualizaciones que necesiten pruebas

Entregable: Informe de vulnerabilidades de dependencias

Parte 3: revisión de código para el OWASP Top 10 (60 minutos)

Elija 3-5 endpoints críticos (inicio de sesión, registro, restablecimiento de contraseña, acceso a datos) y revíselos en busca de:

  • Inyección SQL
  • Vulnerabilidades XSS
  • Controles de autorización ausentes
  • Referencias directas a objetos inseguros
  • Exposición de datos sensibles

Entregable: Lista de hallazgos con calificaciones de severidad

Parte 4: verificación de cabeceras de seguridad (15 minutos)

  1. Visite securityheaders.com
  2. Escanee su URL de producción
  3. Documente las cabeceras faltantes
  4. Implemente las cabeceras faltantes o cree un ticket

Entregable: Informe de cabeceras de seguridad y plan de remediación

Parte 5: crear lista de verificación del equipo (30 minutos)

  1. Adapte la lista de verificación de codificación segura anterior a su stack
  2. Agregue elementos específicos del lenguaje
  3. Comparta con el equipo
  4. Agregue al proceso de revisión de código

Entregable: Lista de verificación de codificación segura específica del equipo

Errores comunes

Error 1: seguridad a través de la oscuridad

Ocultar cosas (puertos no estándar, URLs ofuscadas) en lugar de asegurarlas. La oscuridad añade fricción a los atacantes pero no es seguridad real.

Corrección: Seguro por diseño. Asuma que los atacantes saben todo sobre su sistema excepto los secretos.

Error 2: validación solo del lado del cliente

Confiar en que la validación JavaScript prevendrá las entradas malas.

Corrección: Valide siempre en el servidor. La validación del lado del cliente es solo para la UX.

Error 3: criptografía propia

Escribir su propio cifrado, hashing o generación de tokens.

Corrección: Use librerías establecidas. Use bcrypt para contraseñas. Use la gestión de sesiones de su framework. Use librerías JWT estándar.

Error 4: mensajes de error que ayudan a los atacantes

"Nombre de usuario inválido" vs "Contraseña inválida" le dice a los atacantes qué nombres de usuario existen.

Corrección: Mensajes de error genéricos: "Credenciales inválidas."

Error 5: registrar secretos

Volcar cuerpos de solicitudes en los registros, incluyendo contraseñas y claves API.

Corrección: Filtre los campos sensibles antes de registrar. Nunca registre contraseñas, tokens o claves.

Herramientas para el desarrollo seguro

Análisis estático (SAST)

HerramientaLenguajesNivel gratuito
SemgrepMuchos
BanditPythonCódigo abierto
BrakemanRuby/RailsCódigo abierto
ESLint security pluginsJavaScriptCódigo abierto
SonarQubeMuchosEdición Community
CodeQLMuchosGratuito para código abierto

Escaneo de secretos

HerramientaDescripción
git-secretsHerramienta de AWS, hook pre-commit
gitleaksRápido, configurable
truffleHogEscaneo profundo, secretos verificados
detect-secretsHerramienta de Yelp, buena para CI

Escaneo de dependencias

HerramientaEcosistema
npm auditNode.js
pip-auditPython
bundler-auditRuby
SnykMúltiples, nivel gratuito
DependabotGitHub, múltiples

Cómo explicarlo al liderazgo

Si alguien le pregunta por qué dedica tiempo a la codificación segura:

"Estoy revisando nuestro código en busca de vulnerabilidades de seguridad comunes — los mismos problemas que causan la mayoría de las brechas. Cosas como la inyección SQL y el cross-site scripting son conocidas desde hace décadas, pero siguen apareciendo en el código todo el tiempo. También estoy escaneando en busca de secretos confirmados accidentalmente en nuestro repositorio y comprobando nuestras dependencias en busca de vulnerabilidades conocidas. Esto reduce el riesgo de una brecha y nos ahorra correcciones más costosas más adelante."

Versión corta: "Estoy encontrando y corrigiendo problemas de seguridad en nuestro código antes de que los atacantes los encuentren."

Autocomprobación

Seguridad del código

  • Entender el OWASP Top 10
  • Puede identificar vulnerabilidades de inyección SQL
  • Puede identificar vulnerabilidades XSS
  • Sabe cómo hacer hash de contraseñas correctamente
  • Sabe cómo validar la entrada

Gestión de secretos

  • Repositorio escaneado en busca de secretos
  • Sin secretos en el código fuente
  • Los secretos se cargan desde el entorno o un gestor de secretos
  • Hook pre-commit para prevenir nuevos secretos

Dependencias

  • Ejecutar auditoría de dependencias
  • Sin vulnerabilidades críticas en las dependencias
  • Proceso para mantener las dependencias actualizadas

Proceso

  • Existe una lista de verificación de codificación segura
  • La seguridad se considera en la revisión de código
  • Herramienta de análisis estático en su lugar (o planificada)

Marque al menos 10 de 14 elementos antes de continuar.

Qué sigue

Entiende las vulnerabilidades comunes y cómo prevenirlas en el código. Próximo capítulo: gestión de requisitos de seguridad — cómo definir, rastrear y verificar los requisitos de seguridad usando OWASP ASVS e integrarlos en su flujo de trabajo de desarrollo.