Arribem a una de les famílies de vulnerabilitats més antigues i persistents. A03 – Injecció (Injection) es produeix quan dades controlades per l'usuari es barregen amb una instrucció que un intèrpret executarà —SQL, ordres del sistema operatiu, LDAP, consultes NoSQL— i l'intèrpret acaba executant part d'aquestes dades com si fossin instruccions. En el Top Ten 2021 aquesta categoria va créixer: va absorbir l'antic Cross-Site Scripting (XSS), que tècnicament és una injecció al navegador. Aquí tractarem la injecció del costat servidor; el XSS el desenvolupem a fons a la lliçó 03-04, així que en aquesta només el mencionem com a parent proper.
L'arrel sempre és la mateixa: no separar el codi de les dades. A BazarNube veurem una injecció SQL al cercador de productes de l'API Node/PostgreSQL, i una injecció d'ordres a una utilitat del mòdul legacy Java. L'impacte va des de llegir tota la base de dades fins a executar ordres al servidor.
Avís legal i ètic: els
payloadsmostrats són il·lustratius i amb dades fictícies. Practica només sobre sistemes propis o amb autorització explícita. Llançar injeccions contra sistemes aliens és delicte.
Contingut
- Què és la injecció i per què es produeix
- Injecció SQL (SQLi) a l'API de BazarNube
- Consultes parametritzades i ORM
- Injecció d'ordres del sistema operatiu
- Injecció LDAP i NoSQL
- Validació d'entrada i mínim privilegi
- XSS: la injecció del navegador (remès a 03-04)
- Errors comuns, exercicis i solucions
- Què és la injecció i per què es produeix
Un intèrpret rep una cadena i decideix què és instrucció i què és dada. Si construïm aquesta cadena concatenant entrada de l'usuari, aquest pot introduir metacaràcters (', ;, --, $) que trenquen l'estructura prevista i canvien el significat de la instrucció.
flowchart LR
A[Entrada de l'usuari] --> B{Es concatena a la instruccio?}
B -- Si --> C[L'interpret executa dades com a codi]
B -- No, va com a parametre --> D[La dada mai canvia l'estructura]
C --> E[Injeccio]
D --> F[Segur]
La defensa de fons és separar codi de dades: el desenvolupador defineix l'estructura de la instrucció i el motor tracta l'entrada de l'usuari sempre com a valor, mai com a sintaxi.
- Injecció SQL a BazarNube
El cercador de productes de l'API es va escriure concatenant el terme de cerca:
// routes/search.js — VULNERABLE a SQLi
router.get('/api/products/search', async (req, res) => {
const q = req.query.q;
const sql = "SELECT id, name, price FROM products WHERE name LIKE '%" + q + "%'";
const result = await db.query(sql); // concatenacio directa
res.json(result.rows);
});Com s'explota (il·lustratiu)
Amb una cerca normal ?q=samarreta, la consulta és correcta. Però un atacant envia:
La consulta resultant es converteix en:
SELECT id, name, price FROM products WHERE name LIKE '%x%'
UNION SELECT id, email, pwd FROM users --%'El UNION afegeix als resultats els correus i hashes de contrasenyes de la taula users, i -- comenta la resta. El cercador de productes acaba filtrant credencials. Variants d'aquesta tècnica permeten llegir qualsevol taula, esborrar dades (; DROP TABLE ...) o extreure informació a cegues (blind SQLi) mesurant temps de resposta.
Correcció: consultes parametritzades
// routes/search.js — SEGUR amb consulta parametritzada
router.get('/api/products/search', async (req, res) => {
const q = req.query.q ?? '';
const sql = 'SELECT id, name, price FROM products WHERE name LIKE $1';
const result = await db.query(sql, ['%' + q + '%']); // q va com a PARAMETRE
res.json(result.rows);
});La diferència és essencial: $1 és un marcador de posició. El driver de PostgreSQL envia l'estructura de la consulta i els valors per separat; el motor mai interpreta el contingut de q com a SQL. Encara que q contingui ' UNION SELECT ..., es buscarà literalment aquell text al nom del producte. El codi i les dades queden separats.
- Consultes parametritzades i ORM
| Enfocament | Exemple | Seguretat |
|---|---|---|
| Concatenació de cadenes | "... WHERE id=" + id |
Vulnerable |
| Consulta parametritzada | query(sql, [id]) |
Segur |
| ORM / query builder | Product.findByPk(id) |
Segur (fa servir paràmetres per sota) |
Un ORM com Sequelize o Prisma genera consultes parametritzades automàticament, cosa que redueix el risc. Compte: aquesta protecció es perd si fas servir la "via d'escapament" de l'ORM per a SQL cru concatenant entrada:
// PERILL: fins i tot amb ORM, aixo torna a ser vulnerable
sequelize.query("SELECT * FROM products WHERE name = '" + name + "'");
// Correcte: fer servir replacements/bind
sequelize.query('SELECT * FROM products WHERE name = ?', { replacements: [name] });
- Injecció d'ordres del sistema operatiu
El mòdul legacy Java té una utilitat que genera miniatures invocant una eina externa. Es va construir concatenant el nom de fitxer:
// ThumbnailService.java — VULNERABLE a injeccio d'ordres
public void generate(String filename) throws IOException {
// filename arriba des d'una peticio de l'usuari
String cmd = "convert /uploads/" + filename + " -resize 100x100 /thumbs/" + filename;
Runtime.getRuntime().exec(cmd); // executa una cadena a la shell
}Si filename és foto.png; rm -rf /data, la shell executa dues ordres. L'atacant aconsegueix execució d'ordres al servidor. Correcció: no invocar una shell i passar els arguments per separat, a més de validar el nom.
// ThumbnailService.java — SEGUR
public void generate(String filename) throws IOException, InterruptedException {
if (!filename.matches("[A-Za-z0-9_.-]{1,64}")) { // allowlist estricta
throw new IllegalArgumentException("Nom de fitxer invalid");
}
ProcessBuilder pb = new ProcessBuilder(
"convert", "/uploads/" + filename, "-resize", "100x100", "/thumbs/" + filename);
// ProcessBuilder NO fa servir shell: cada argument es passa tal qual, sense interpretar ; | &
pb.start().waitFor();
}ProcessBuilder amb arguments separats evita que la shell interpreti metacaràcters, i la llista blanca (matches) rebutja qualsevol nom que no sigui alfanumèric. Doble barrera.
- Injecció LDAP i NoSQL
El mateix principi (separar codi de dades) aplica a altres intèrprets:
- LDAP: construir filtres concatenant entrada permet injectar
*)(uid=*per saltar autenticacions. Solució: escapar segons RFC 4515 o fer servir APIs que parametritzin el filtre. - NoSQL (MongoDB): enviar objectes en lloc de cadenes permet operadors. Si el login fa
find({user, pass})ambpassagafat tal qual del JSON, un atacant envia{"pass": {"$ne": null}}i aconsegueix "qualsevol contrasenya diferent de null".
// NoSQL — VULNERABLE: accepta objectes del client
db.users.findOne({ user: req.body.user, pass: req.body.pass });
// SEGUR: forcar tipus primitius abans de consultar
const user = String(req.body.user), pass = String(req.body.pass);
db.users.findOne({ user, pass }); // (i a mes comparar hash, veure A02)Forçar els tipus (String(...)) impedeix que el client colgui operadors com $ne o $gt.
- Validació d'entrada i mínim privilegi
La parametrització és la defensa principal, però es reforça amb defensa en profunditat:
- Validació d'entrada per llista blanca: accepta només allò esperat (tipus, longitud, format). No confiïs en "llistes negres" de caràcters prohibits; sempre hi ha una manera de saltar-les.
- Mínim privilegi a la base de dades: el compte que fa servir l'API de BazarNube no cal que sigui superusuari ni pugui fer
DROP TABLE. Si només llegeix/escriu en certes taules, una SQLi té molt menys abast. - Escapament com a últim recurs: quan no puguis parametritzar (p. ex. un nom de columna dinàmic), valida contra una llista blanca de valors permesos, mai concatenis directament.
- Limitar resultats i errors: no retornis errors SQL crus al client (faciliten l'explotació; veure A05).
| Capa | Què aporta |
|---|---|
| Consulta parametritzada / ORM | Elimina la injecció d'origen |
| Validació per allowlist | Rebutja entrada malformada abans d'arribar al motor |
| Mínim privilegi a la BD | Redueix l'impacte si alguna cosa falla |
| Errors genèrics | No donen pistes a l'atacant |
- XSS: la injecció del navegador
El Cross-Site Scripting també és injecció, però l'intèrpret és el navegador (executa JavaScript injectat) en lloc de la base de dades. Per la seva importància i per les seves tècniques pròpies (codificació de sortida contextual, CSP, escapaments de React) el tractem en la seva pròpia lliçó: 03-04 – Cross-Site Scripting (XSS) en Profunditat. Aquí només deixem constància que comparteix la mateixa arrel: barrejar dades de l'usuari amb codi que un altre motor executarà.
Errors Comuns i Consells
- Concatenar entrada en consultes. Encara que "sembli un número", parametritza sempre.
- Confiar en llistes negres de caràcters. Són evitables; fes servir llistes blanques i parametrització.
- Escapar a mà el SQL. És fràgil; deixa l'escapament al driver mitjançant paràmetres.
- Fer servir l'ORM i després caure en
raw queryconcatenada. Anul·la tota la protecció. - Passar entrada a una shell. Evita
Runtime.exec(string)/child_process.exec; fes servir APIs amb arguments separats. - Compte de BD amb permisos totals. Aplica mínim privilegi: limita taules i operacions.
- Consell: habilita un escàner SAST/DAST en CI (ho veurem amb ZAP a M6) per detectar concatenacions perilloses.
Exercicis
Exercici 1. Reescriu de manera segura aquest endpoint de BazarNube:
router.get('/api/orders', async (req, res) => {
const status = req.query.status;
const rows = await db.query(
"SELECT * FROM orders WHERE user_id = " + req.session.userId +
" AND status = '" + status + "'");
res.json(rows.rows);
});Exercici 2. El login del mòdul Java fa servir "... WHERE user='" + u + "' AND pass='" + p + "'". Mostra un payload que salti l'autenticació i explica per què la parametrització l'evita.
Exercici 3. Per què ProcessBuilder("convert", filename, ...) és més segur que Runtime.exec("convert " + filename)? Continua sent recomanable validar filename?
Solucions
Solució 1. Parametritzar tots dos valors (inclòs user_id, encara que vingui de la sessió, per consistència):
router.get('/api/orders', async (req, res) => {
const rows = await db.query(
'SELECT * FROM orders WHERE user_id = $1 AND status = $2',
[req.session.userId, req.query.status]);
res.json(rows.rows);
});Solució 2. Amb u = admin' -- la consulta passa a ... WHERE user='admin' --' AND pass='...': el -- comenta la comprovació de contrasenya i l'atacant entra com a admin. Amb paràmetres, admin' -- es buscaria literalment com a nom d'usuari (no existeix), perquè el motor mai interpreta aquella cadena com a sintaxi SQL.
Solució 3. ProcessBuilder amb arguments separats no llança una shell, així que metacaràcters com ;, | o & es passen com a text literal a convert, no s'interpreten com a separadors d'ordres. Tot i així convé validar filename per llista blanca: evita rutes malicioses (../../etc/passwd) i entrades que trenquin l'eina. Defensa en profunditat.
Conclusió
La injecció es derrota separant codi de dades: consultes parametritzades o ORM per a SQL, APIs amb arguments separats per a ordres del SO, forçat de tipus per a NoSQL i escapament adequat per a LDAP, tot reforçat amb validació per llista blanca i mínim privilegi. És una defensa mecànica i molt efectiva: ben aplicada, elimina la categoria gairebé per complet.
Entrada de backlog — A03: parametritzat el cercador de productes i el llistat de comandes; substituït Runtime.exec per ProcessBuilder amb validació a ThumbnailService; forçat de tipus al login NoSQL; creat un compte de BD amb permisos mínims per a l'API.
Hem mencionat que el XSS és la injecció que s'executa al navegador. És tan rellevant per al front React de BazarNube que li dediquem la lliçó següent sencera: Cross-Site Scripting (XSS) en Profunditat.
Curs d'OWASP: Directrius i Estàndards per a la Seguretat en Aplicacions Web
Mòdul 1: Introducció a OWASP
Mòdul 2: Principals Projectes d'OWASP
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Altres Projectes Clau: WSTG, Cheat Sheets i Dependency-Check
Mòdul 3: OWASP Top Ten 2021 en Profunditat
- A01:2021 – Pèrdua de Control d'Accés
- A02:2021 – Errors Criptogràfics i Exposició de Dades Sensibles
- A03:2021 – Injecció
- Cross-Site Scripting (XSS) en Profunditat
- A04:2021 – Disseny Insegur
- A05:2021 – Configuració de Seguretat Incorrecta
- Entitats Externes XML (XXE)
- A06:2021 – Components Vulnerables i Desactualitzats
- A07:2021 – Errors d'Identificació i Autenticació
- A08:2021 – Errors d'Integritat de Programari i Dades (Deserialització Insegura)
- A09:2021 – Errors de Registre i Monitorització
- A10:2021 – Server-Side Request Forgery (SSRF)
Mòdul 4: OWASP ASVS (Application Security Verification Standard)
Mòdul 5: OWASP SAMM (Software Assurance Maturity Model)
Mòdul 6: OWASP ZAP (Zed Attack Proxy)
- Introducció a ZAP
- Instal·lació i Configuració
- Escaneig de Vulnerabilitats
- Automatització de Proves de Seguretat
Mòdul 7: Bones Pràctiques i Recomanacions
- Cicle de Vida de Desenvolupament Segur (SDLC)
- Modelatge d'Amenaces (Threat Modeling)
- Integració de Seguretat en DevOps (DevSecOps)
- Formació i Conscienciació en Seguretat
- Eines i Recursos Addicionals
Mòdul 8: Exercicis Pràctics i Casos d'Estudi
- Exercici 1: Identificació de Vulnerabilitats
- Exercici 2: Implementació de Controls de Seguretat
- Cas d'Estudi 1: Anàlisi d'un Incident de Seguretat
- Cas d'Estudi 2: Millora de la Seguretat en una Aplicació Web
