Seguridad en APIs REST: Los 10 Errores Más Comunes y Cómo Evitarlos
Guía técnica de seguridad ofensiva y defensiva para APIs REST. Ejemplos reales en Python (FastAPI) y Laravel. Desde autenticación rota hasta exposición de datos sensibles, con código de ataque y defensa.

Seguridad en APIs REST: Los 10 Errores Más Comunes y Cómo Evitarlos
En el artículo anterior construimos una API REST completa con FastAPI y Vue 3. Ahora vamos a intentar romperla.
Este artículo nació de un proceso que aplico en todos mis proyectos — incluyendo DevGuardian AI, mi plataforma de detección de vulnerabilidades: una vez que el backend está funcionando, lo analizo como si fuera un atacante. Qué endpoints quedan expuestos. Qué datos se filtran. Qué pasa si alguien manipula los tokens o fuerza los IDs.
Lo que encontré me cambió la forma de escribir código.
La mayoría de las vulnerabilidades en APIs REST no son exploits sofisticados. Son errores de diseño que cualquier dev comete cuando prioriza "que funcione" sobre "que sea seguro". Este artículo cubre los diez más frecuentes — con ejemplos de código vulnerable, el ataque que los explota, y la corrección.
Referencia base: OWASP API Security Top 10 — el estándar de la industria para vulnerabilidades en APIs.
⚠️ Aviso: Todo el código de ataque en este artículo es para entornos controlados y propósitos educativos. Aplicar estas técnicas en sistemas sin autorización explícita es ilegal.
1. Broken Object Level Authorization (BOLA) — El más peligroso
OWASP API1:2023
BOLA es consistentemente el #1 en el ranking de OWASP — y con razón. Ocurre cuando la API expone endpoints que operan sobre recursos identificados por ID sin verificar que el usuario autenticado tiene permiso sobre ese recurso específico.
El código vulnerable
# ❌ VULNERABLE — FastAPI
@router.get("/tasks/{task_id}")
def get_task(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# Busca la tarea por ID sin verificar que pertenece al usuario
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return task
El ataque
# Usuario A está autenticado y sabe que su tarea tiene el ID 42
# Simplemente itera IDs para ver tareas de otros usuarios:
for i in {1..1000}; do
curl -s -H "Authorization: Bearer TOKEN_USUARIO_A" \
https://api.ejemplo.com/api/v1/tasks/$i | jq '.title, .owner_id'
done
El atacante puede leer —y potencialmente modificar o eliminar— datos de cualquier usuario del sistema con un simple loop. No necesita credenciales robadas ni exploits. Solo cambiar un número en la URL.
La corrección
# ✅ CORRECTO — siempre filtrar por owner_id además del ID del recurso
@router.get("/tasks/{task_id}")
def get_task(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
task = db.query(Task).filter(
Task.id == task_id,
Task.owner_id == current_user.id # ← esta línea es la diferencia
).first()
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found",
)
return task
Regla: En cada endpoint que opere sobre un recurso específico, el filtro debe incluir siempre el identificador del usuario autenticado. Nunca confíes en el ID de la URL como garantía de autorización.
2. Broken Authentication — JWT mal implementado
OWASP API2:2023
JWT es el mecanismo de autenticación más usado en APIs modernas — y también uno de los más mal implementados. Los errores más comunes no están en el algoritmo, sino en la validación.
Error 1: Aceptar el algoritmo none
# ❌ VULNERABLE — no especificar algoritmos permitidos
payload = jwt.decode(token, SECRET_KEY)
# Un atacante puede forjar un token así:
# Header: {"alg": "none", "typ": "JWT"}
# Payload: {"sub": "1", "type": "access"}
# Signature: (vacía)
# El token pasa la validación si no especificas algoritmos
# ✅ CORRECTO — siempre especificar la lista de algoritmos permitidos
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"] # lista explícita, nunca omitir
)
Error 2: No verificar el tipo de token
# ❌ VULNERABLE — usar el refresh token como access token
def decode_token(token: str) -> TokenData:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return TokenData(user_id=payload.get("sub"))
# No verifica si es access o refresh — ambos son intercambiables
# Un atacante con un refresh token expirado puede intentar usarlo
# como access token si no verificas el campo "type"
# ✅ CORRECTO — validar el claim "type" explícitamente
def decode_access_token(token: str) -> TokenData:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
if payload.get("type") != "access":
raise ValueError("Token type inválido — se esperaba access token")
user_id = payload.get("sub")
if not user_id:
raise ValueError("Token sin subject")
return TokenData(user_id=int(user_id))
Error 3: Secret key débil o hardcodeada
# ❌ VULNERABLE
SECRET_KEY = "secret" # trivialmente bruteforceable
SECRET_KEY = "mysupersecret123" # en texto plano en el código
# ✅ CORRECTO — generar con entropía suficiente y leer desde env
# Generar: openssl rand -hex 32
SECRET_KEY = os.environ.get("SECRET_KEY") # mínimo 32 bytes aleatorios
if not SECRET_KEY:
raise RuntimeError("SECRET_KEY no configurada")
3. Broken Object Property Level Authorization — Mass Assignment
OWASP API3:2023
Ocurre cuando el cliente puede modificar propiedades del objeto que no debería controlar — como su propio rol, estado de verificación, o saldo.
El código vulnerable
# ❌ VULNERABLE — Laravel ejemplo
public function update(Request $request, User $user)
{
// Acepta cualquier campo que el cliente envíe
$user->update($request->all());
return response()->json($user);
}
# El atacante envía campos que no debería poder modificar:
curl -X PUT https://api.ejemplo.com/api/v1/users/me \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Usuario Normal",
"role": "admin",
"is_verified": true,
"credits": 99999
}'
La corrección
# ✅ CORRECTO — FastAPI con schema estricto
class UserUpdateSchema(BaseModel):
# Solo los campos que el usuario puede modificar
name: str | None = Field(default=None, max_length=100)
bio: str | None = Field(default=None, max_length=500)
# Campos como 'role', 'is_verified', 'credits' NO están aquí
@router.put("/users/me", response_model=UserResponse)
def update_profile(
data: UserUpdateSchema, # Pydantic valida y filtra automáticamente
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
for field, value in data.model_dump(exclude_unset=True).items():
setattr(current_user, field, value)
db.flush()
return current_user
// ✅ CORRECTO — Laravel con fillable explícito
class User extends Model
{
// Solo estos campos son asignables masivamente
protected $fillable = ['name', 'bio'];
// Nunca en $fillable: role, is_admin, is_verified, balance
}
public function update(Request $request)
{
$validated = $request->validate([
'name' => 'sometimes|string|max:100',
'bio' => 'sometimes|string|max:500',
]);
auth()->user()->update($validated);
}
4. Unrestricted Resource Consumption — Sin Rate Limiting
OWASP API4:2023
Sin rate limiting, tu API es vulnerable a fuerza bruta en login, enumeración de usuarios, spam de endpoints costosos, y ataques de denegación de servicio básicos.
El ataque
# Fuerza bruta en el endpoint de login sin ningún límite:
while true; do
curl -s -X POST https://api.ejemplo.com/api/v1/auth/login \
-d "username=victima@email.com&password=$(shuf -n1 wordlist.txt)"
done
# Con una wordlist de 10,000 passwords comunes y sin rate limit,
# esto tarda minutos en ejecutarse
La corrección en FastAPI con slowapi
# requirements: pip install slowapi
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import Request
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# Aplicar límites específicos por endpoint
@router.post("/auth/login")
@limiter.limit("5/minute") # máximo 5 intentos por minuto por IP
async def login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
...
@router.post("/auth/register")
@limiter.limit("3/hour") # registros limitados por hora
async def register(request: Request, user_data: UserCreate):
...
@router.get("/tasks")
@limiter.limit("60/minute") # endpoints normales más permisivos
async def get_tasks(request: Request):
...
Rate limiting en Laravel con middleware
// routes/api.php
Route::middleware(['throttle:5,1'])->group(function () {
// 5 requests por minuto
Route::post('/auth/login', [AuthController::class, 'login']);
});
Route::middleware(['throttle:60,1'])->group(function () {
// 60 requests por minuto para endpoints normales
Route::apiResource('tasks', TaskController::class);
});
Además del rate limiting por IP, considera:
5. Broken Function Level Authorization — Endpoints Admin Expuestos
OWASP API5:2023
La API expone endpoints de administración sin verificar correctamente que el usuario tiene el rol necesario — o peor, confía en que el cliente no va a "descubrir" las rutas.
El código vulnerable
# ❌ VULNERABLE — verificación de rol inconsistente
@router.delete("/admin/users/{user_id}")
def delete_user(
user_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
# Verifica autenticación pero no el rol
user = db.query(User).filter(User.id == user_id).first()
db.delete(user)
# Cualquier usuario autenticado puede eliminar cualquier cuenta
El ataque
# El atacante descubre rutas admin con fuzzing:
ffuf -u https://api.ejemplo.com/api/v1/FUZZ \
-w /usr/share/wordlists/api-endpoints.txt \
-H "Authorization: Bearer TOKEN_USUARIO_NORMAL" \
-mc 200,201,204
# Resultado: /admin/users, /admin/stats, /admin/config
# Todos accesibles con un token de usuario regular
La corrección
# ✅ CORRECTO — dependency específica para admin
from fastapi import HTTPException, status
def require_admin(current_user: User = Depends(get_current_user)) -> User:
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
return current_user
# Aplicar en todos los endpoints admin
@router.delete("/admin/users/{user_id}")
def delete_user(
user_id: int,
admin: User = Depends(require_admin), # ← falla si no es admin
db: Session = Depends(get_db),
):
...
Principio clave: La seguridad por oscuridad no existe. Asumir que nadie va a "descubrir" /admin/ es ingenuidad. Los roles deben verificarse en el servidor en cada request, no en el cliente.
6. Unrestricted Access to Sensitive Business Flows
OWASP API6:2023
Algunos flujos de negocio son costosos o sensibles por naturaleza — crear cuentas, aplicar cupones, generar reportes, procesar pagos. Sin protecciones adicionales, se pueden automatizar y abusar.
Ejemplos de abuso real
# Abuso de sistema de referidos:
# Si crear una cuenta genera créditos para el referidor,
# un script puede crear miles de cuentas falsas:
for i in {1..1000}; do
curl -X POST https://api.ejemplo.com/api/v1/auth/register \
-d "{\"email\": \"fake$i@tempmail.com\", \"referral_code\": \"VICTIMA123\"}"
done
# Abuso de cupones de descuento de un solo uso:
# Sin validación atómica, race conditions permiten aplicar
# el mismo cupón múltiples veces en requests simultáneos
La corrección
# ✅ Validación atómica con bloqueo en base de datos
# Para cupones de un solo uso — usando SELECT FOR UPDATE
@router.post("/checkout/apply-coupon")
def apply_coupon(
coupon_code: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
# FOR UPDATE previene race conditions — bloquea el registro
coupon = (
db.query(Coupon)
.filter(Coupon.code == coupon_code, Coupon.is_used == False)
.with_for_update() # ← bloqueo a nivel de fila
.first()
)
if not coupon:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Coupon invalid or already used",
)
coupon.is_used = True
coupon.used_by = current_user.id
# get_db() hace el commit al cerrar — atómico
return {"discount": coupon.discount_percent}
7. Server Side Request Forgery (SSRF)
OWASP API7:2023
Ocurre cuando la API acepta URLs controladas por el usuario y hace requests desde el servidor — permitiendo al atacante explorar la red interna, acceder a metadata de cloud, o hacer pivoting.
El código vulnerable
# ❌ VULNERABLE — acepta URLs sin validación
@router.post("/import/url")
async def import_from_url(url: str, current_user: User = Depends(get_current_user)):
import httpx
response = await httpx.get(url) # El servidor hace el request
return process_data(response.content)
# El atacante apunta a la metadata de AWS EC2:
curl -X POST https://api.ejemplo.com/api/v1/import/url \
-d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# O a servicios internos no expuestos:
curl -X POST https://api.ejemplo.com/api/v1/import/url \
-d '{"url": "http://interno.empresa.local/admin/config"}'
# O a Redis si está en la red interna sin autenticación:
curl -X POST https://api.ejemplo.com/api/v1/import/url \
-d '{"url": "http://redis:6379/"}'
La corrección
# ✅ CORRECTO — validar y restringir URLs permitidas
import ipaddress
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"https"}
BLOCKED_HOSTS = {
"localhost", "127.0.0.1", "0.0.0.0",
"169.254.169.254", # AWS metadata
"metadata.google.internal", # GCP metadata
}
def validate_url(url: str) -> str:
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError(f"Scheme no permitido: {parsed.scheme}")
hostname = parsed.hostname or ""
if hostname in BLOCKED_HOSTS:
raise ValueError("Host bloqueado")
# Bloquear IPs privadas y de loopback
try:
ip = ipaddress.ip_address(hostname)
if ip.is_private or ip.is_loopback or ip.is_link_local:
raise ValueError("IP privada no permitida")
except ValueError as e:
if "not a valid" not in str(e):
raise
return url
@router.post("/import/url")
async def import_from_url(
url: str,
current_user: User = Depends(get_current_user),
):
try:
safe_url = validate_url(url)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
)
async with httpx.AsyncClient(
follow_redirects=False, # no seguir redirects — pueden bypassear la validación
timeout=5.0,
) as client:
response = await client.get(safe_url)
return process_data(response.content)
8. Security Misconfiguration — CORS mal configurado
OWASP API8:2023
CORS mal configurado es uno de los errores más comunes y más fáciles de cometer. Un wildcard o una validación laxa puede permitir que cualquier sitio web haga requests autenticados a tu API desde el browser de un usuario.
Configuraciones peligrosas
# ❌ VULNERABLE — wildcard con credenciales (inválido pero intentado)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True, # Los browsers bloquean esto, pero el intento es un error de diseño
)
# ❌ VULNERABLE — validación de origin con startswith (bypasseable)
def is_allowed_origin(origin: str) -> bool:
return origin.startswith("https://miapp.com")
# "https://miapp.com.attacker.com" pasa esta validación
La corrección
# ✅ CORRECTO — lista blanca explícita y estricta
import os
ALLOWED_ORIGINS = os.environ.get("ALLOWED_ORIGINS", "").split(",")
# En producción: ALLOWED_ORIGINS=https://miapp.com,https://www.miapp.com
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # lista explícita, nunca "*"
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
max_age=600, # cachear preflight 10 minutos
)
Otras misconfiguraciones frecuentes:
# ❌ Exponer stack trace en producción
app = FastAPI(debug=True) # Nunca en producción
# ❌ Headers de servidor que revelan tecnología
# Por defecto FastAPI incluye: server: uvicorn
# Nginx debería sobreescribir esto:
# server_tokens off;
# ❌ Endpoints de documentación expuestos en producción
app = FastAPI(
docs_url=None, # ✅ Deshabilitar en producción
redoc_url=None,
openapi_url=None,
)
9. Improper Inventory Management — Versiones viejas expuestas
OWASP API9:2023
Las APIs acumulan versiones. El problema es que /api/v1/ se "retira" pero sigue funcionando — con la seguridad del 2021, sin los fixes del 2024.
El problema
# La empresa lanzó v2 con autenticación mejorada.
# v1 sigue activa "por compatibilidad con clientes viejos":
curl https://api.ejemplo.com/api/v1/users/me # sin auth → funciona
curl https://api.ejemplo.com/api/v2/users/me # sin auth → 401
# El atacante simplemente usa v1. Toda la seguridad de v2 es irrelevante.
# También común: endpoints de desarrollo o testing en producción:
curl https://api.ejemplo.com/api/v1/debug/users # lista todos los usuarios
curl https://api.ejemplo.com/api/v1/health/db # expone info de la DB
La corrección
# ✅ Deprecación activa con headers de aviso
from fastapi import Response
from datetime import datetime, timezone
DEPRECATED_AFTER = datetime(2026, 12, 31, tzinfo=timezone.utc)
def deprecation_warning(response: Response):
"""Dependency que añade headers de deprecación."""
response.headers["Deprecation"] = DEPRECATED_AFTER.isoformat()
response.headers["Sunset"] = DEPRECATED_AFTER.strftime("%a, %d %b %Y %H:%M:%S GMT")
response.headers["Link"] = '</api/v2>; rel="successor-version"'
# Aplicar a todos los endpoints de v1
v1_router = APIRouter(
prefix="/api/v1",
dependencies=[Depends(deprecation_warning)],
)
# Checklist de inventario de API — ejecutar regularmente:
# 1. Listar todos los endpoints activos
curl https://api.ejemplo.com/api/openapi.json | jq '.paths | keys[]'
# 2. Verificar que endpoints de debug no estén en producción
grep -r "debug\|test\|dev\|internal" routes/
# 3. Documentar qué versiones están activas y cuándo se retiran
10. Unsafe Consumption of APIs — Confiar ciegamente en APIs externas
OWASP API10:2023
Tu API seguramente consume otras APIs — de pagos, de mapas, de autenticación social, de proveedores de datos. Si confías ciegamente en esas respuestas sin validar, un proveedor comprometido puede inyectar datos maliciosos en tu sistema.
El código vulnerable
# ❌ VULNERABLE — confiar sin validar en respuesta de API externa
@router.post("/auth/google")
async def google_oauth(token: str):
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://oauth2.googleapis.com/tokeninfo?id_token={token}"
)
user_data = response.json()
# Usa directamente email y name sin validar
user = get_or_create_user(email=user_data["email"], name=user_data["name"])
return create_session(user)
La corrección
# ✅ CORRECTO — validar estructura y contenido de respuestas externas
from pydantic import BaseModel, EmailStr
class GoogleTokenInfo(BaseModel):
"""Schema estricto para la respuesta de Google."""
email: EmailStr
email_verified: bool
name: str
sub: str # Google user ID
aud: str # debe coincidir con tu CLIENT_ID
@field_validator("email_verified")
@classmethod
def email_must_be_verified(cls, v: bool) -> bool:
if not v:
raise ValueError("Email no verificado por Google")
return v
GOOGLE_CLIENT_ID = os.environ["GOOGLE_CLIENT_ID"]
@router.post("/auth/google")
async def google_oauth(token: str):
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
"https://oauth2.googleapis.com/tokeninfo",
params={"id_token": token},
)
if response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token de Google inválido",
)
try:
token_info = GoogleTokenInfo.model_validate(response.json())
except ValidationError:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Respuesta de Google con formato inesperado",
)
# Verificar que el token fue emitido para NUESTRA aplicación
if token_info.aud != GOOGLE_CLIENT_ID:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token no emitido para esta aplicación",
)
user = get_or_create_user(email=token_info.email, name=token_info.name)
return create_session(user)
Checklist de seguridad para APIs REST
Antes de hacer deploy de cualquier API a producción, este es el checklist mínimo:
### Autenticación y Autorización
- [ ] Todos los endpoints protegidos requieren token válido
- [ ] Cada recurso verifica que pertenece al usuario autenticado (anti-BOLA)
- [ ] Los endpoints admin tienen dependency de rol separada
- [ ] JWT valida algoritmo, tipo de token y expiración
- [ ] SECRET_KEY tiene mínimo 32 bytes de entropía aleatoria
### Inputs y Validación
- [ ] Todos los inputs pasan por schemas Pydantic / Form Request de Laravel
- [ ] No hay `request->all()` ni `**request.dict()` sin filtrado explícito
- [ ] URLs externas se validan contra lista de hosts/IPs privadas
- [ ] Respuestas de APIs externas se validan con schemas estrictos
### Rate Limiting y Disponibilidad
- [ ] Endpoints de auth tienen rate limiting estricto (≤5/min por IP)
- [ ] Endpoints costosos tienen límites adicionales
- [ ] Timeouts configurados en todos los clientes HTTP externos
### Configuración
- [ ] CORS con lista blanca explícita, nunca "*" con credentials
- [ ] Debug mode desactivado en producción
- [ ] Docs (Swagger/ReDoc) deshabilitados en producción o protegidos
- [ ] Headers de servidor no revelan versiones (server_tokens off)
- [ ] Variables sensibles en env vars, nunca hardcodeadas
### Inventario
- [ ] Versiones de API antiguas tienen fecha de sunset definida
- [ ] Endpoints de debug/test no existen en producción
- [ ] Hay un registro actualizado de todos los endpoints públicos
Herramientas recomendadas
Para análisis automatizado antes de cada release:
Análisis estático:
# Bandit — escanear el directorio del proyecto
pip install bandit
bandit -r ./app -ll # -ll solo reporta medium y high severity
# Semgrep — con reglas de OWASP
semgrep --config=p/owasp-top-ten ./app
Análisis dinámico (black-box):
# Nuclei contra tu API local
nuclei -u http://localhost:8000 -t nuclei-templates/http/
Para CI/CD — GitHub Actions:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install bandit
- run: bandit -r ./app -ll --exit-zero -f json -o bandit-report.json
- uses: actions/upload-artifact@v4
with:
name: bandit-report
path: bandit-report.json
Conclusión
La seguridad en APIs no es un feature que se agrega al final — es una propiedad del diseño que se construye desde el principio. Los diez errores de este artículo no requieren conocimiento avanzado de criptografía ni de sistemas operativos. Requieren disciplina: revisar cada endpoint con la pregunta "¿qué pasa si alguien intenta abusar esto?"
El proceso que sigo en DevGuardian AI es simple: después de implementar cualquier endpoint nuevo, lo trato como atacante durante 10 minutos. ¿Puedo acceder a datos de otros usuarios? ¿Puedo bypasear la autorización? ¿Puedo hacer que el servidor haga algo que no debería?
Ese ejercicio mental ha encontrado más vulnerabilidades que cualquier herramienta automatizada.
El checklist de arriba es el punto de partida. No es exhaustivo — la seguridad nunca lo es — pero cubre el 80% de lo que los atacantes intentan primero.
Recursos
¿Encontraste una vulnerabilidad en algún ejemplo de este artículo o tienes algo que agregar? La discusión técnica es bienvenida — podés encontrarme en GitHub o desde mi portfolio.