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
- De la troballa al control: el flux de remediació
- Remediació per troballa (codi corregit)
- Taula mestra: troballa → control → ASVS → verificació
- Verificació: reescaneig i regressió
- Errors comuns i consells
- Exercicis
- 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:
- 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. - 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 el400; 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 tancatErrors Comuns i Consells
- Arreglar el símptoma. Escapar cometes a mà, posar un WAF al davant o filtrar
../amb unreplacesó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-134era 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ó:
É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
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Altres Projectes Clau: WSTG, Cheat Sheets i Dependency-Check
Mòdul 3: OWASP Top Ten 2021 en Profunditat
- A01:2021 – Pèrdua de Control d'Accés
- A02:2021 – Errors Criptogràfics i Exposició de Dades Sensibles
- A03:2021 – Injecció
- Cross-Site Scripting (XSS) en Profunditat
- A04:2021 – Disseny Insegur
- A05:2021 – Configuració de Seguretat Incorrecta
- Entitats Externes XML (XXE)
- A06:2021 – Components Vulnerables i Desactualitzats
- A07:2021 – Errors d'Identificació i Autenticació
- A08:2021 – Errors d'Integritat de Programari i Dades (Deserialització Insegura)
- A09:2021 – Errors de Registre i Monitorització
- A10:2021 – Server-Side Request Forgery (SSRF)
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)
- Introducció a ZAP
- Instal·lació i Configuració
- Escaneig de Vulnerabilitats
- Automatització de Proves de Seguretat
Mòdul 7: Bones Pràctiques i Recomanacions
- Cicle de Vida de Desenvolupament Segur (SDLC)
- Modelatge d'Amenaces (Threat Modeling)
- Integració de Seguretat en DevOps (DevSecOps)
- Formació i Conscienciació en Seguretat
- Eines i Recursos Addicionals
Mòdul 8: Exercicis Pràctics i Casos d'Estudi
- Exercici 1: Identificació de Vulnerabilitats
- Exercici 2: Implementació de Controls de Seguretat
- Cas d'Estudi 1: Anàlisi d'un Incident de Seguretat
- Cas d'Estudi 2: Millora de la Seguretat en una Aplicació Web
