A la lliçó anterior vam identificar nou vulnerabilitats en un fragment de BazarNube i les vam deixar registrades com a fitxes BZN-xxx al backlog. Trobar el problema és només la meitat de la feina; l'altra meitat —la que de veritat redueix el risc— és remediar-lo bé. En aquest laboratori agafem aquestes mateixes nou fitxes i, una a una, implementem el control correcte: codi corregit i comentat, mapeig al requisit ASVS que satisfà, i verificació que la correcció funciona mitjançant un reescaneig amb ZAP i proves dirigides. La regla de l'exercici és la que regeix la feina real d'AppSec: una troballa no es tanca fins que es verifica. Reutilitzem sense reexplicar els conceptes ja vistos —consultes parametritzades (03-03), autorització per objecte (03-01), capçaleres i helmet (03-06), criptografia (03-02), ASVS (M4) i ZAP (M6)—; aquí els apliquem.

Contingut

  1. De la troballa al control: el flux de remediació
  2. Remediació per troballa (codi corregit)
  3. Taula mestra: troballa → control → ASVS → verificació
  4. Verificació: reescaneig i regressió
  5. Errors comuns i consells
  6. Exercicis
  7. Conclusió

De la troballa al control: el flux de remediació

Cada fitxa del backlog recorre el mateix cicle fins a poder marcar-se com a tancada:

graph LR
    A[Troballa BZN oberta] --> B[Triar el control correcte]
    B --> C[Implementar codi segur]
    C --> D[Mapejar a requisit ASVS]
    D --> E[Verificar: reescaneig i prova]
    E --> F{Verificat?}
    F -->|Si| G[Tancar BZN]
    F -->|No| C

Dos principis guien l'elecció del control:

  • Corregir la causa, no el símptoma. Filtrar una cometa no arregla una SQLi; parametritzar la consulta sí. Busquem el control que elimina la classe sencera de fallada.
  • Defensa en capes. Quan és barat, es combinen controls (validar entrada i parametritzar i mínim privilegi) perquè una fallada aïllada no basti.

Remediació per troballa (codi corregit)

BZN-134 — SQLi al cercador (A03 → parametrització)

La causa és concatenar q a la cadena SQL. El control és una consulta parametritzada: les dades viatgen a part de la sentència i mai no s'interpreten com a codi.

// catalog.js — corregit
router.get('/api/products/search', async (req, res) => {
  const q = String(req.query.q ?? '').slice(0, 100); // validacio de tipus i longitud
  const sql = 'SELECT id, name, price FROM products WHERE name ILIKE $1';
  const { rows } = await db.query(sql, [`%${q}%`]);   // el valor va parametritzat
  res.json(rows);
});

El % s'afegeix al valor, no a la sentència: el driver de PostgreSQL l'escapa. A més validem tipus i longitud (defensa en capes). Satisfà ASVS V5.3.4 (consultes parametritzades).

BZN-101 — IDOR en comandes (A01 → autorització per objecte)

El control és comprovar que l'objecte pertany a l'usuari autenticat. Es fa a la mateixa consulta per evitar condicions de cursa i oblits.

// orders.js — corregit
router.get('/api/comandes/:id', auth, async (req, res) => {
  const { rows } = await db.query(
    'SELECT * FROM orders WHERE id = $1 AND user_id = $2', // lligat al propietari
    [req.params.id, req.user.id]
  );
  if (rows.length === 0) return res.status(404).json({ error: 'No trobat' });
  res.json(rows[0]);
});

Retornem 404 (no 403) per no revelar l'existència de la comanda aliena. Satisfà ASVS V4.2.1 (autorització a nivell d'objecte).

BZN-140 — Path traversal en factures (A01 → canonització i whitelist)

Mai no s'uneix l'entrada de l'usuari a un path sense normalitzar. Es valida el nom i es comprova que la ruta resolta queda dins del directori permès.

// orders.js — corregit
const INVOICES_DIR = '/var/bazarnube/invoices';
router.get('/api/invoices', auth, async (req, res) => {
  const name = String(req.query.file ?? '');
  if (!/^[0-9]{4}-[0-9]{6}\.pdf$/.test(name)) {      // whitelist de format
    return res.status(400).json({ error: 'Nom invalid' });
  }
  const full = path.resolve(INVOICES_DIR, name);
  if (!full.startsWith(INVOICES_DIR + path.sep)) {   // la ruta no escapa del directori
    return res.status(400).json({ error: 'Ruta no permesa' });
  }
  // A mes: comprovar que la factura pertany a req.user abans de servir-la
  res.sendFile(full);
});

Doble control: whitelist del format i verificació que la ruta canonitzada no escapa. Satisfà ASVS V12.3.1 / V12.3.2.

BZN-131 — SSRF en importar producte (A10 → validació de destí)

El control davant de SSRF és no deixar que l'usuari dicti a on crida el servidor: validar esquema i domini contra una allowlist i rebutjar IPs internes.

// catalog.js — corregit
const ALLOWED_HOSTS = new Set(['images.bazarnube.com', 'cdn.proveedor.com']);
router.post('/api/products/import', auth, async (req, res) => {
  let url;
  try { url = new URL(req.body.imageUrl); } catch { return res.status(400).end(); }
  if (url.protocol !== 'https:' || !ALLOWED_HOSTS.has(url.hostname)) {
    return res.status(400).json({ error: 'Origen no permes' }); // bloqueja metadata, localhost, etc.
  }
  const resp = await fetch(url, { redirect: 'error' }); // no seguir redireccions a destins interns
  const buffer = Buffer.from(await resp.arrayBuffer());
  res.json({ ok: true });
});

L'allowlist i el bloqueig de redireccions impedeixen arribar a 169.254.169.254 o serveis interns. Satisfà ASVS V12.6.1 (protecció SSRF).

BZN-087 — XSS emmagatzemat en ressenyes (A03 → escapar/sanititzar sortida)

La fallada és dangerouslySetInnerHTML amb contingut d'usuari. La correcció ideal és deixar que React escapi el text; si cal format, es sanititza amb una llibreria.

// ProductReviews.jsx — corregit
export function ProductReviews({ reviews }) {
  return (
    <ul>
      {reviews.map((r) => (
        <li key={r.id}>{r.body}</li> // React escapa per defecte: no hi ha HTML injectable
      ))}
    </ul>
  );
}

Si el negoci exigeix negreta o enllaços, s'usa DOMPurify.sanitize(r.body) amb una allowlist d'etiquetes, mai l'HTML cru. Satisfà ASVS V5.3.3 (codificació de sortida contextual).

BZN-155 — Secret JWT encastat i feble (A02 → gestió de secrets)

Els secrets surten del codi i es carreguen de l'entorn; s'exigeix una longitud mínima.

// config.js — corregit
const jwtSecret = process.env.JWT_SECRET; // injectat pel gestor de secrets
if (!jwtSecret || jwtSecret.length < 32) {
  throw new Error('JWT_SECRET absent o massa curt');
}
module.exports = {
  jwtSecret,
  jwtAlg: 'HS256',
  db: { host: 'db', user: 'app', password: process.env.DB_PASSWORD, ssl: true },
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
};

El secret ja no viu a Git (a més el bloquejaria gitleaks, 07-03) i la connexió a BD usa TLS. Satisfà ASVS V6.4.1 / V2.10.4 (gestió de secrets).

BZN-041 i BZN-039 — Capçaleres i cookies (A05 → helmet i flags)

Un sol canvi a l'arrencada resol les dues fitxes de configuració: helmet afegeix CSP i capçaleres, i les cookies s'emeten amb els tres flags.

// app.js — corregit
const helmet = require('helmet');
app.use(helmet({
  contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"] } },
}));
// La cookie de sessio s'emet amb els flags definits a config.cookie:
// res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'lax' });

Satisfà ASVS V14.4.x (capçaleres) i V3.4.x (atributs de cookie).

BZN-060 — Fuites per errors verbosos (A05 → gestió d'errors)

El client rep un missatge genèric; el detall va al log intern (base per a la detecció de 03-11).

// app.js — corregit
app.use((err, req, res, next) => {
  logger.error({ msg: err.message, stack: err.stack, reqId: req.id }); // detall nomes al log
  res.status(500).json({ error: 'Error intern', reqId: req.id });      // res sensible al client
});

El reqId permet correlacionar sense exposar res. Satisfà ASVS V7.4.1 (no filtrar informació sensible en errors).

Taula mestra: troballa → control → ASVS → verificació

Aquesta taula és el lliurable de l'exercici: la traçabilitat completa de cada fitxa, del problema a la prova.

Fitxa Categoria Control implementat Requisit ASVS Com es verifica
BZN-134 A03 Consulta parametritzada + validació V5.3.4 ZAP actiu: l'alerta SQLi desapareix; q=' OR 1=1-- no altera resultats
BZN-101 A01 Filtre user_id a la consulta V4.2.1 Prova amb 2 usuaris: A no veu la comanda de B (404)
BZN-140 A01 Whitelist + path canonitzat V12.3.1 ?file=../../etc/passwd retorna 400; ZAP no reporta traversal
BZN-131 A10 Allowlist de host + no redirect V12.6.1 imageUrl=http://169.254.169.254/... retorna 400
BZN-087 A03 Escapat per defecte de React V5.3.3 Ressenya <img onerror=...> es mostra com a text, no s'executa
BZN-155 A02 Secret a l'entorn, longitud mínima V6.4.1 gitleaks no troba secrets; l'arrencada falla sense JWT_SECRET
BZN-041 A05 helmet + CSP V14.4.x ZAP passiu: "CSP Header Not Set" desapareix
BZN-039 A05 Flags HttpOnly/Secure/SameSite V3.4.x Inspecció Set-Cookie; ZAP no marca cookie insegura
BZN-060 A05 Error genèric + log intern V7.4.1 Provocar 500: resposta sense stack; el detall és al log

Verificació: reescaneig i regressió

Tancar una fitxa exigeix evidència que la correcció funciona, no la paraula del desenvolupador. Apliquem dues comprovacions complementàries:

  1. Reescaneig ZAP sobre el staging amb el fix desplegat. Reutilitzem el baseline de 06-04: les alertes que abans sortien (SQL Injection, Path Traversal, CSP Header Not Set, cookie insegura, error disclosure) han de desaparèixer de l'informe. Si una persisteix, la correcció no va arribar a la ruta escanejada.
  2. Proves dirigides per al que ZAP no veu: l'IDOR (BZN-101) es verifica amb dos usuaris reals; el SSRF (BZN-131), llançant la petició a un destí intern i comprovant el 400; el secret (BZN-155), passant gitleaks al pipeline.

Aquestes proves de verificació es converteixen en tests de regressió: entren a la suite de CI (07-03) perquè la fallada no reaparegui en un canvi futur. A BazarNube, cada BZN-xxx tancat deixa rere seu un test que el vigila. Així l'arranjament d'avui no es desfà demà.

# Reescaneig baseline en CI, reutilitzant la config de 06-04
docker run -t ghcr.io/zaproxy/zaproxy zap-baseline.py \
  -t https://staging.bazarnube.com \
  -c zap-baseline.conf \
  -r reporte-verificacion.html
# El pipeline falla si reapareix qualsevol alerta que ja haviem tancat

Errors Comuns i Consells

  • Arreglar el símptoma. Escapar cometes a mà, posar un WAF al davant o filtrar ../ amb un replace són pedaços fràgils. Ataca la causa: parametritzar, canonitzar, autoritzar.
  • Corregir sense verificar. Un fix sense reescaneig ni prova no està acabat; molts "arranjaments" no cobreixen totes les rutes o es despleguen malament.
  • Oblidar la defensa en capes. Validar l'entrada està bé, però no substitueix parametritzar la consulta. Combina controls quan el cost és baix.
  • No deixar test de regressió. Sense una prova que el vigili, la fallada torna al següent refactor. Cada tancament hauria de generar el seu test.
  • Consell: remedia per classe de fallada, no per instància. Si BZN-134 era una SQLi per concatenació, busca les altres concatenacions del codi i corregeix-les totes; probablement n'hi ha més de la mateixa família.

Exercicis

Exercici 1. L'equip vol reforçar el cercador ja parametritzat (BZN-134) amb validació d'entrada. Escriu la validació (tipus i longitud) i explica per què és defensa en capes i no un substitut de la consulta parametritzada.

Exercici 2. Per al SSRF (BZN-131), un company proposa "bloquejar les URLs que continguin localhost o 127.0.0.1" amb un filtre de text. Explica per què aquesta allowlist-per-negació és insuficient i quin enfocament és correcte.

Exercici 3. Després de desplegar els fixos, el reescaneig ZAP continua reportant "CSP Header Not Set" en una ruta concreta. Enumera tres causes possibles i com ho investigaries.

Solucions

Solució 1. Validació:

const q = String(req.query.q ?? '').trim().slice(0, 100);
if (q.length === 0) return res.json([]);

És defensa en capes perquè redueix la superfície (rebutja entrades absurdes, limita longitud per evitar abusos de rendiment), però no és la protecció contra SQLi: encara que un atacant enviés una q vàlida en longitud amb contingut maliciós, la parametrització és la que impedeix que s'interpreti com a SQL. La validació complementa, no reemplaça; confiar només en ella (per exemple, amb una blacklist de paraules SQL) seria evadible.

Solució 2. Filtrar per text localhost/127.0.0.1 és una blacklist trivialment evadible: existeixen 0.0.0.0, [::1], 127.0.0.2, la IP decimal 2130706433, DNS que resol a intern (DNS rebinding), redireccions a destins interns, i el metadata 169.254.169.254. Enumerar el dolent sempre deixa forats. L'enfocament correcte és una allowlist d'esquema (https) i de hosts permesos, més redirect: 'error' i, si es pot, resoldre la IP i rebutjar rangs privats/link-local. Es permet el conegut bo, no es persegueix el dolent.

Solució 3. Tres causes possibles: (1) el fix no cobreix aquella ruta —helmet s'aplica abans d'un router muntat a part que respon sense passar pel middleware—; (2) una resposta cacheada o un proxy/CDN intermedi serveix la versió antiga sense la capçalera; (3) aquella ruta la serveix un altre servei (el legacy Java) que no du helmet. Investigació: curl -I directe a l'endpoint per veure les capçaleres reals, comprovar l'ordre dels middlewares a app.js, i verificar si la ruta l'atén Express o el backend legacy.

Conclusió

Hem tancat el cicle complet sobre les nou fitxes: per cadascuna, el control que ataca la causa, el requisit ASVS que el sustenta i la verificació —reescaneig ZAP o prova dirigida— que demostra que funciona, més el test de regressió que evita que reaparegui. Aquesta és la feina real d'AppSec: no n'hi ha prou amb trobar, cal remediar bé i provar la remediació. Els exercicis 08-01 i 08-02 han recorregut el cicle defensiu de principi a fi sobre codi concret. Ara canviem de perspectiva: en lloc de prevenir al laboratori, a la lliçó 08-03 investiguem què passa quan una d'aquestes vulnerabilitats no es corregeix a temps i acaba en un incident real. Analitzarem una fuita de dades a BazarNube: la seva cronologia, com es va detectar, la causa arrel i quin control OWASP l'hauria evitat.

Curs d'OWASP: Directrius i Estàndards per a la Seguretat en Aplicacions Web

Mòdul 1: Introducció a OWASP

Mòdul 2: Principals Projectes d'OWASP

Mòdul 3: OWASP Top Ten 2021 en Profunditat

Mòdul 4: OWASP ASVS (Application Security Verification Standard)

Mòdul 5: OWASP SAMM (Software Assurance Maturity Model)

Mòdul 6: OWASP ZAP (Zed Attack Proxy)

Mòdul 7: Bones Pràctiques i Recomanacions

Mòdul 8: Exercicis Pràctics i Casos d'Estudi

Mòdul 9: Avaluació i Certificació

© Copyright 2026. Tots els drets reservats