Los 10 riesgos más críticos para la seguridad de las aplicaciones web y cómo mitigarlos.
El OWASP Top 10 es el documento de concienciación más reconocido en materia de seguridad de aplicaciones web. La edición de 2025 refleja el panorama de amenazas más reciente, introduciendo nuevas categorías como Fallos en la cadena de suministro de software y Mala gestión de condiciones excepcionales.
El control de acceso aplica la política de forma que los usuarios no puedan actuar fuera de los permisos previstos. Los fallos suelen provocar la divulgación no autorizada de información, la modificación o destrucción de datos, o la realización de una función empresarial fuera de los límites del usuario.
Los atacantes pueden aprovechar los fallos de control de acceso para acceder a las cuentas de otros usuarios, ver archivos confidenciales, modificar los datos de otros usuarios o cambiar los derechos de acceso.
# User ID taken directly from request without authorization check @app.route('/api/user/<user_id>/profile') def get_profile(user_id): user = db.get_user(user_id) # No ownership check! return jsonify(user.to_dict())
@app.route('/api/user/<user_id>/profile') @login_required def get_profile(user_id): # Verify the requesting user owns this resource if current_user.id != user_id and not current_user.is_admin: abort(403) user = db.get_user(user_id) if not user: abort(404) return jsonify(user.to_dict())
La mala configuración de la seguridad se produce cuando los ajustes de seguridad se definen, implementan o mantienen de forma incorrecta. Esto incluye la falta de refuerzo de la seguridad, la activación de funciones innecesarias, cuentas predeterminadas con contraseñas sin modificar, mensajes de error demasiado detallados y cabeceras de seguridad HTTP mal configuradas.
Los servidores, frameworks o servicios en la nube mal configurados pueden exponer datos sensibles, permitir accesos no autorizados o proporcionar a los atacantes información para planificar nuevos ataques.
# Debug mode enabled in production, verbose errors exposed app = Flask(__name__) app.config['DEBUG'] = True # Exposes stack traces! app.config['SECRET_KEY'] = 'default-secret' # Default key!
import os app = Flask(__name__) app.config['DEBUG'] = False app.config['SECRET_KEY'] = os.environ['SECRET_KEY'] @app.after_request def set_security_headers(response): response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = "default-src 'self'" return response
Nuevo en 2025. Se centra en los riesgos relacionados con los componentes de terceros, las dependencias y las canalizaciones CI/CD. Los atacantes atacan la cadena de suministro de software comprometiendo paquetes, inyectando código malicioso en bibliotecas de código abierto o explotando vulnerabilidades de las canalizaciones de compilación.
Una sola dependencia comprometida puede afectar a miles de aplicaciones. Los ataques a la cadena de suministro pueden conducir al robo de datos, la instalación de puertas traseras o el compromiso completo del sistema con una detección mínima.
// package.json with unpinned dependencies { "dependencies": { "express": "*", // Any version! "lodash": "^4.0.0", // Wide range "unknown-pkg": "^1.0.0" // Unvetted package } }
// package.json with pinned versions + lockfile + audit { "dependencies": { "express": "4.21.2", "lodash": "4.17.21" }, "scripts": { "preinstall": "npx npm-audit-resolver", "integrity-check": "npm audit signatures" } } // Also: use package-lock.json, enable Dependabot/Renovate, // generate SBOM, verify package provenance
Fallos relacionados con la criptografía que a menudo conducen a la exposición de datos sensibles. Esto incluye el uso de algoritmos obsoletos, la generación de claves débiles, la falta de cifrado de datos en tránsito o en reposo y la validación incorrecta de certificados.
Un cifrado deficiente o inexistente puede dejar al descubierto contraseñas, números de tarjetas de crédito, historiales médicos, información personal y secretos comerciales, lo que puede dar lugar a infracciones de la normativa (GDPR, PCI DSS).
import hashlib # Storing passwords with weak hashing def store_password(password: str) -> str: return hashlib.md5(password.encode()).hexdigest() # MD5 is broken!
import bcrypt def store_password(password: str) -> bytes: # Use bcrypt with automatic salting salt = bcrypt.gensalt(rounds=12) return bcrypt.hashpw(password.encode(), salt) def verify_password(password: str, hashed: bytes) -> bool: return bcrypt.checkpw(password.encode(), hashed)
Los fallos de inyección se producen cuando se envían datos no fiables a un intérprete como parte de un comando o consulta. Las formas más comunes son la inyección SQL, la inyección NoSQL, la inyección de comandos OS y el Cross-Site Scripting (XSS). Los datos hostiles pueden engañar al intérprete para que ejecute comandos no deseados.
La inyección puede provocar pérdida de datos, corrupción, acceso no autorizado, toma completa del host o denegación de servicio. La inyección SQL por sí sola sigue siendo uno de los vectores de ataque más peligrosos y frecuentes.
# SQL injection via string concatenation def get_user(username: str): query = f"SELECT * FROM users WHERE name = '{username}'" return db.execute(query) # username = "' OR '1'='1"
# Parameterized queries prevent SQL injection def get_user(username: str): query = "SELECT * FROM users WHERE name = %s" return db.execute(query, (username,)) # For XSS prevention, escape output from markupsafe import escape def render_comment(comment: str) -> str: return f"<p>{escape(comment)}</p>"
El diseño inseguro se refiere a los riesgos relacionados con defectos de diseño y arquitectura. Exige el uso de modelos de amenazas, patrones de diseño seguros y arquitecturas de referencia. Un diseño inseguro no puede solucionarse con una implementación perfecta; los controles de seguridad necesarios nunca se crearon para defenderse de ataques específicos.
Los fallos de diseño pueden dar lugar a vulnerabilidades de la lógica empresarial difíciles de detectar con herramientas automatizadas. La omisión de límites de velocidad en operaciones sensibles, la falta de autenticación multifactor para acciones críticas o la insuficiente detección del fraude son fallos a nivel de diseño.
# Password reset with no rate limit or verification @app.route('/reset-password', methods=['POST']) def reset_password(): email = request.form['email'] new_pass = request.form['new_password'] user = db.find_by_email(email) user.password = hash_password(new_pass) # No token verification! db.save(user)
from datetime import datetime, timedelta @app.route('/reset-password', methods=['POST']) @rate_limit("3/hour") def reset_password(): token = request.form['token'] new_pass = request.form['new_password'] # Verify time-limited, single-use token reset_req = db.find_reset_token(token) if not reset_req or reset_req.used or reset_req.expires < datetime.utcnow(): abort(400, "Invalid or expired token") # Enforce password complexity if not meets_password_policy(new_pass): abort(400, "Password does not meet requirements") reset_req.user.password = hash_password(new_pass) reset_req.used = True db.save_all([reset_req.user, reset_req])
La confirmación de la identidad de un usuario, la autenticación y la gestión de sesiones son fundamentales. Los fallos de autenticación se producen cuando las aplicaciones permiten el relleno de credenciales, la fuerza bruta, las contraseñas débiles o tienen una gestión de sesión defectuosa, como la no rotación de los ID de sesión tras el inicio de sesión.
Los atacantes pueden acceder a las cuentas de los usuarios mediante el relleno automático de credenciales, la fuerza bruta o el secuestro de sesiones. Las cuentas comprometidas pueden dar lugar a robos de identidad, fraudes y filtraciones de datos.
# No brute force protection, weak session handling @app.route('/login', methods=['POST']) def login(): user = db.find_by_email(request.form['email']) if user and user.password == request.form['password']: # Plain comparison! session['user'] = user.id # Session ID not rotated return redirect('/dashboard')
from flask_limiter import Limiter limiter = Limiter(app, default_limits=["100/hour"]) @app.route('/login', methods=['POST']) @limiter.limit("5/minute") def login(): user = db.find_by_email(request.form['email']) if not user or not bcrypt.checkpw( request.form['password'].encode(), user.password_hash ): # Generic error to prevent user enumeration return "Invalid credentials", 401 # Rotate session ID after authentication session.regenerate() session['user'] = user.id # Check for MFA requirement if user.mfa_enabled: return redirect('/mfa-verify') return redirect('/dashboard')
Los fallos de integridad del software y los datos están relacionados con el código y la infraestructura que no protegen contra las violaciones de la integridad. Esto incluye la deserialización insegura, el uso de CDN o plugins no fiables sin verificación de integridad y mecanismos de actualización automática sin actualizaciones firmadas.
La deserialización insegura puede llevar a la ejecución remota de código. La carga de secuencias de comandos de CDN no fiables sin integridad de sub-recursos (SRI) puede permitir a los atacantes inyectar código malicioso en su aplicación.
<!-- Loading external scripts without integrity checks --> <script src="https://cdn.example.com/lib.js"></script> <!-- Insecure deserialization in Python --> import pickle data = pickle.loads(user_input) # Arbitrary code execution!
<!-- Use Subresource Integrity (SRI) for external scripts --> <script src="https://cdn.example.com/lib.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K..." crossorigin="anonymous"></script> # Use safe deserialization in Python import json data = json.loads(user_input) # Safe: only parses JSON data
Sin un registro, supervisión y alerta suficientes, las brechas no pueden detectarse a tiempo. Un registro insuficiente, una integración ineficaz con los sistemas de respuesta a incidentes y la falta de alertas en tiempo real permiten a los agresores seguir atacando los sistemas, mantener la persistencia y manipular o extraer datos.
Sin un registro adecuado, los atacantes pueden operar sin ser detectados durante largos periodos. La mayoría de los estudios sobre infracciones muestran que el tiempo medio para detectar una infracción supera los 200 días, a menudo descubierta por partes externas en lugar de por la supervisión interna.
# No logging of security-relevant events @app.route('/login', methods=['POST']) def login(): user = authenticate(request.form) if not user: return "Login failed", 401 # No record of failure return redirect('/dashboard')
import logging from datetime import datetime security_log = logging.getLogger('security') @app.route('/login', methods=['POST']) def login(): user = authenticate(request.form) if not user: security_log.warning( "LOGIN_FAILED | ip=%s | email=%s | time=%s", request.remote_addr, request.form.get('email', 'unknown'), datetime.utcnow().isoformat(), ) check_brute_force(request.remote_addr) return "Invalid credentials", 401 security_log.info( "LOGIN_SUCCESS | user_id=%s | ip=%s", user.id, request.remote_addr, ) return redirect('/dashboard')
Nuevo en 2025. Las aplicaciones que manejan incorrectamente errores, excepciones y casos extremos pueden exponer información sensible, entrar en estados inconsistentes o crear condiciones explotables. Esto incluye mensajes de error detallados en producción, excepciones no capturadas que eluden los controles de seguridad y condiciones de carrera.
La gestión inadecuada de errores puede dejar al descubierto rastros de pila, detalles de la base de datos o rutas internas. Las condiciones de carrera en las comprobaciones de seguridad pueden permitir ataques de tiempo de comprobación a tiempo de uso (TOCTOU), eludiendo la autorización o la validación del pago.
# Exposing internal details in error messages @app.route('/api/data') def get_data(): try: result = db.query(request.args['q']) return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 # Leaks DB details!
import uuid, logging logger = logging.getLogger(__name__) @app.errorhandler(Exception) def handle_exception(e): error_id = str(uuid.uuid4()) logger.error("Unhandled exception [%s]: %s", error_id, e, exc_info=True) # Return generic error with reference ID for support return jsonify({ "error": "An internal error occurred", "reference": error_id, }), 500 @app.route('/api/data') def get_data(): q = request.args.get('q') if not q or not is_valid_query(q): return jsonify({"error": "Invalid query parameter"}), 400 result = db.query(q) return jsonify(result)
| ID | Vulnerabilidad | Gravedad | Mitigación clave |
|---|---|---|---|
| A01 | Control de acceso defectuoso | Critical | Comprobaciones de acceso en el servidor, denegación por defecto, registro de propiedad |
| A02 | Desconfiguración de la seguridad | High | Proceso de endurecimiento, cabeceras de seguridad, desactivar valores por defecto |
| A03 | Fallos en la cadena de suministro de software | High | Dependencias ancladas, SBOM, exploración de dependencias |
| A04 | Fallos criptográficos | High | Algoritmos robustos, TLS 1.2+, gestión de claves |
| A05 | Inyección | Critical | Consultas parametrizadas, codificación de la salida, validación de la entrada |
| A06 | Diseño inseguro | High | Modelado de amenazas, patrones seguros, pruebas de casos de abuso |
| A07 | Fallos de autenticación | High | MFA, protección por fuerza bruta, rotación de sesión |
| A08 | Fallos en el software o en la integridad de los datos | High | SRI, actualizaciones firmadas, deserialización segura |
| A09 | Fallos en el registro y las alertas de seguridad | Medium | Registro centralizado, alertas en tiempo real, respuesta a incidentes |
| A10 | Mala gestión de las condiciones excepcionales | Medium | Gestor global de errores, mensajes genéricos, prevención de TOCTOU |