SQL injection er stadig en af de mest udbredte sårbarheder i webapplikationer i 2026. En enkelt usikker databaseforespørgsel giver angribere adgang til at læse, ændre eller slette hele din database uden at kende et eneste password. OWASP placerer injection-angreb som A03 i Top 10 over de farligste webapplikationssårbarheder, og Node.js-applikationer er ikke immune. En analyse fra Stackademic i januar 2026 dokumenterer, at SQL injection stadig ses overalt i produktionskode, på trods af at løsningerne er veldokumenterede og tilgængelige.
Denne guide viser dig præcis, hvordan du eliminerer SQL injection i Node.js trin for trin. Du lærer at bruge parameteriserede forespørgsler med mysql2 og pg, sikre ORMs som Prisma, inputvalidering med Zod og express-validator, og en række avancerede teknikker der beskytter din applikation i produktionsmiljøet. Guiden er baseret på research fra 2025-2026 og dækker den kode, du skriver i dag.
Hvad er SQL injection, og hvorfor er det farligt i 2026?
SQL injection opstår, når brugerinput flettes direkte ind i en SQL-forespørgsel som tekst. Databasemotoren kan ikke skelne mellem forespørgselslogik og brugerdata, og en angriber kan dermed injicere sin egen SQL-kode. Konsekvenserne spænder fra datalæk til fuldstændig overtagelse af serveren.
Tag dette Node.js-eksempel, der aldrig må bruges i produktion:
// FARLIGT - Brug aldrig dette mønster
const username = req.body.username;
const query = `SELECT * FROM users WHERE username = '${username}'`;
connection.query(query, (err, results) => {
// En angriber sender: admin' OR '1'='1
// SQL bliver: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
// Resultat: Alle brugere returneres
});
En angriber indsender admin' OR '1'='1 som brugernavn, og forespørgslen returnerer alle brugere i databasen. Med et lidt mere avanceret payload som '; DROP TABLE users; -- sletter angriberen hele tabellen.
| Angrebstype | Eksempel-payload | Effekt | Løsning |
|---|---|---|---|
| Classic injection | ' OR '1'='1 | Omgå login | Parameteriserede queries |
| UNION-baseret | ' UNION SELECT password FROM users-- | Dataudtræk | Parameteriserede queries og ORM |
| Blind (boolean) | ' AND 1=1-- | Skjult dataudtræk | Input-validering og WAF |
| Time-based blind | '; WAITFOR DELAY '0:0:5'-- | Bekræft sårbarhed | Query-timeout og parameterisering |
| Stacked queries | '; INSERT INTO admin VALUES(...)-- | Datamanipulation | Deaktiver multiple statements |
| Out-of-band | ' EXEC xp_cmdshell('ping attacker.com')-- | Kommandoudførelse | Mindste privilegium og WAF |
Forudsætninger og versioner
Inden du begynder, skal du have følgende installeret og konfigureret:
- Node.js 22.x LTS eller nyere (aktiv support i 2026)
- npm 10.x eller nyere
- MySQL 8.x eller PostgreSQL 16.x (eksempler dækker begge)
- Grundlæggende kendskab til Express.js og async/await
- En lokal MySQL/PostgreSQL-installation til testformål
Pakker du installerer i denne guide:
| Pakke | Formål | Installation |
|---|---|---|
| express | HTTP-server | npm install express |
| mysql2 | MySQL-driver med parameterisering | npm install mysql2 |
| pg | PostgreSQL-driver | npm install pg |
| @prisma/client | Type-sikker ORM | npm install @prisma/client |
| prisma | Prisma CLI (dev-afhængighed) | npm install --save-dev prisma |
| zod | Schema-validering | npm install zod |
| express-validator | Express middleware-validering | npm install express-validator |
| dotenv | Miljøvariabler | npm install dotenv |
Trin 1: Projektopsætning
Start med at oprette et rent projekt med den korrekte mappestruktur. En god struktur gør det nemmere at håndhæve sikkerhedsregler konsekvent på tværs af al kode.
mkdir node-sql-sikker && cd node-sql-sikker
npm init -y
npm install express mysql2 pg @prisma/client zod express-validator dotenv
npm install --save-dev prisma nodemon
# Opret projektstruktur
mkdir -p src/{routes,middleware,db,validators}
touch src/app.js src/db/mysql.js src/db/postgres.js .env
Tilføj følgende til din .env-fil. Commit aldrig denne fil til versionsstyring:
# .env
DB_HOST=localhost
DB_PORT=3306
DB_USER=app_user # Brug IKKE root
DB_PASSWORD=dit_stærke_password
DB_NAME=node_sikker_db
DB_POOL_MAX=10
PG_HOST=localhost
PG_PORT=5432
PG_USER=app_pg_user
PG_PASSWORD=dit_postgres_password
PG_DATABASE=node_sikker_pg
echo ".env" >> .gitignore
echo "node_modules/" >> .gitignore
Trin 2: Forstå den sårbare kode
Inden du retter et problem, skal du forstå det til bunds. Her er tre typiske sårbare mønstre du ofte ser i Node.js-kode, og som du skal genkende med det samme:
// FARLIGE MØNSTRE - Vis kun til undervisningsformål
// Mønster 1: Template literals med brugerinput
const søg = req.query.q;
db.query(`SELECT * FROM produkter WHERE navn LIKE '%${søg}%'`);
// Mønster 2: String-sammensætning
const id = req.params.id;
db.query("SELECT * FROM brugere WHERE id = " + id);
// Mønster 3: Ufiltreret sortering (kan ikke parameteriseres)
const kolonne = req.query.sortBy;
db.query(`SELECT * FROM ordrer ORDER BY ${kolonne} ASC`);
// Angriber sender: kolonne = "1; DROP TABLE ordrer; --"
Det tredje mønster er særligt svært at opdage, fordi kolonnenavne ikke kan parameteriseres direkte. Du skal i stedet bruge allowlisting (trin 10) til sorteringskolonner og andre dynamiske strukturer.
Trin 3: Parameteriserede forespørgsler med mysql2
mysql2-pakken er den anbefalede MySQL-driver til Node.js i 2026. Den understøtter parameteriserede forespørgsler som standard og bruger ?-pladsholdere. Værdier sendes som et separat array, aldrig flettet ind i SQL-strengen.
Opret din database-forbindelsesopsætning i src/db/mysql.js:
// src/db/mysql.js
require('dotenv').config();
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: parseInt(process.env.DB_POOL_MAX),
multipleStatements: false, // KRITISK: deaktiver multiple statements
waitForConnections: true,
queueLimit: 0,
});
module.exports = pool;
Brug nu parameteriserede forespørgsler i alle dine routes:
// src/routes/brugere.js
const express = require('express');
const pool = require('../db/mysql');
const router = express.Router();
// SIKKERT: Parameteriseret forespørgsel med ?-pladsholder
router.get('/bruger/:id', async (req, res) => {
const { id } = req.params;
try {
// id indsættes som parameter, ikke i SQL-strengen
const [rows] = await pool.execute(
'SELECT id, navn, email FROM brugere WHERE id = ?',
[id]
);
if (rows.length === 0) {
return res.status(404).json({ fejl: 'Bruger ikke fundet' });
}
res.json(rows[0]);
} catch (err) {
// Afslør ALDRIG databasefejl til klienten
console.error('Databasefejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
// SIKKERT: Søgning med LIKE
router.get('/søg', async (req, res) => {
const { q } = req.query;
try {
// Indpak wildcards på serversiden, ikke klientsiden
const søgterm = `%${q}%`;
const [rows] = await pool.execute(
'SELECT id, navn, email FROM brugere WHERE navn LIKE ?',
[søgterm]
);
res.json(rows);
} catch (err) {
console.error('Søgningsfejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
module.exports = router;
Bemærk forskellen på pool.query() og pool.execute(): execute() bruger prepared statements på protokolniveau, hvilket giver det stærkeste forsvar. Brug altid execute() med brugerinput.
Trin 4: Parameteriserede forespørgsler med pg (PostgreSQL)
PostgreSQL bruger $1, $2, $3-pladsholdere i stedet for ?. pg-pakken håndterer parameterisering korrekt, når du adskiller SQL-tekst fra parametre.
// src/db/postgres.js
require('dotenv').config();
const { Pool } = require('pg');
const pgPool = new Pool({
host: process.env.PG_HOST,
port: parseInt(process.env.PG_PORT),
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DATABASE,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
module.exports = pgPool;
// src/routes/produkter.js
const pgPool = require('../db/postgres');
const router = require('express').Router();
router.get('/produkt/:id', async (req, res) => {
const { id } = req.params;
try {
// PostgreSQL-parameterisering med $1-pladsholder
const result = await pgPool.query(
'SELECT id, navn, pris, kategori FROM produkter WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
return res.status(404).json({ fejl: 'Produkt ikke fundet' });
}
res.json(result.rows[0]);
} catch (err) {
console.error('PostgreSQL-fejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
// Indsæt data sikkert med flere parametre
router.post('/produkt', async (req, res) => {
const { navn, pris, kategori } = req.body;
try {
const result = await pgPool.query(
'INSERT INTO produkter (navn, pris, kategori) VALUES ($1, $2, $3) RETURNING id',
[navn, pris, kategori]
);
res.status(201).json({ id: result.rows[0].id });
} catch (err) {
console.error('Indsætningsfejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
Trin 5: Prisma ORM til automatisk parameterisering
Prisma er en type-sikker ORM der automatisk parameteriserer alle forespørgsler. Du skriver aldrig rå SQL i normale operationer, og Prisma håndterer alle databindinger korrekt. Det er en af de stærkeste beskyttelser mod SQL injection, fordi fejlmuligheden elimineres strukturelt i stedet for at afhænge af, at udvikleren husker at gøre det korrekt.
Opsæt Prisma til MySQL:
# Initialiser Prisma
npx prisma init --datasource-provider mysql
# Generer Prisma Client efter schema-ændringer
npx prisma generate
# Push schema til databasen (kun i udvikling)
npx prisma db push
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Bruger {
id Int @id @default(autoincrement())
navn String @db.VarChar(100)
email String @unique @db.VarChar(255)
oprettet DateTime @default(now())
@@map("brugere")
}
model Produkt {
id Int @id @default(autoincrement())
navn String @db.VarChar(200)
pris Decimal @db.Decimal(10, 2)
kategori String @db.VarChar(50)
@@map("produkter")
}
// src/routes/prisma-brugere.js
const { PrismaClient } = require('@prisma/client');
const router = require('express').Router();
const prisma = new PrismaClient();
// SIKKERT: Prisma parameteriserer automatisk via query-API
router.get('/bruger/:id', async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ fejl: 'Ugyldigt ID' });
}
try {
const bruger = await prisma.bruger.findUnique({
where: { id },
select: { id: true, navn: true, email: true, oprettet: true }
});
if (!bruger) {
return res.status(404).json({ fejl: 'Bruger ikke fundet' });
}
res.json(bruger);
} catch (err) {
console.error('Prisma-fejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
// Sikker søgning med Prisma - automatisk escaped
router.get('/søg', async (req, res) => {
const { q } = req.query;
try {
const brugere = await prisma.bruger.findMany({
where: {
navn: {
contains: q,
mode: 'insensitive'
}
},
select: { id: true, navn: true, email: true }
});
res.json(brugere);
} catch (err) {
console.error('Søgningsfejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
// Raw SQL med Prisma - brug KUN tagget template literal
router.get('/avanceret/:kategori', async (req, res) => {
const { kategori } = req.params;
try {
// prisma.$queryRaw med tagged template er sikker
// Brug ALDRIG prisma.$queryRawUnsafe() med brugerinput
const produkter = await prisma.$queryRaw`
SELECT id, navn, pris
FROM produkter
WHERE kategori = ${kategori}
ORDER BY pris ASC
`;
res.json(produkter);
} catch (err) {
console.error('Raw query fejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
Trin 6: Input-validering med Zod
Parameteriserede forespørgsler stopper SQL injection på databaseniveau. Input-validering stopper det allerede ved API-grænsen, inden data overhovedet når databaselaget. Disse to lag supplerer hinanden og må ikke erstatte hinanden. Zod er et TypeScript-first schema-valideringsbibliotek der fungerer fremragende med ren JavaScript. Det validerer ikke bare at en værdi er til stede, men kontrollerer type, format, længde og begrænsninger præcist.
// src/validators/bruger-validator.js
const { z } = require('zod');
const brugerSøgSchema = z.object({
q: z
.string()
.min(2, 'Søgning skal være mindst 2 tegn')
.max(100, 'Søgning må maksimalt være 100 tegn')
.regex(/^[a-zA-ZæøåÆØÅ0-9\s\-\.]+$/, 'Ugyldige tegn i søgning'),
});
const brugerIdSchema = z.object({
id: z
.string()
.regex(/^\d+$/, 'ID skal være et heltal')
.transform(Number)
.refine((n) => n > 0 && n < 2147483647, 'ID uden for gyldigt interval'),
});
const opretBrugerSchema = z.object({
navn: z
.string()
.min(2, 'Navn skal være mindst 2 tegn')
.max(100, 'Navn må maksimalt være 100 tegn')
.regex(/^[a-zA-ZæøåÆØÅ\s\-']+$/, 'Ugyldige tegn i navn'),
email: z
.string()
.email('Ugyldig e-mailadresse')
.max(255, 'E-mail er for lang')
.toLowerCase(),
alder: z
.number()
.int('Alder skal være et heltal')
.min(13, 'Minimum alder er 13')
.max(120, 'Ugyldig alder'),
});
module.exports = { brugerSøgSchema, brugerIdSchema, opretBrugerSchema };
// src/middleware/valider.js
const { ZodError } = require('zod');
function valider(schema, kilde = 'body') {
return (req, res, next) => {
try {
const data = schema.parse(req[kilde]);
req[kilde] = data; // Udskift med valideret og transformeret data
next();
} catch (err) {
if (err instanceof ZodError) {
return res.status(400).json({
fejl: 'Valideringsfejl',
detaljer: err.errors.map((e) => ({
felt: e.path.join('.'),
besked: e.message,
})),
});
}
next(err);
}
};
}
module.exports = valider;
Brug middleware i dine routes:
// src/routes/sikker-brugere.js
const router = require('express').Router();
const pool = require('../db/mysql');
const valider = require('../middleware/valider');
const { brugerSøgSchema, opretBrugerSchema } = require('../validators/bruger-validator');
// Valideringsfejl returneres INDEN databasekaldet sker
router.get('/søg', valider(brugerSøgSchema, 'query'), async (req, res) => {
const { q } = req.query; // Garanteret valideret og renset
const søgterm = `%${q}%`;
const [rows] = await pool.execute(
'SELECT id, navn, email FROM brugere WHERE navn LIKE ?',
[søgterm]
);
res.json(rows);
});
router.post('/opret', valider(opretBrugerSchema), async (req, res) => {
const { navn, email, alder } = req.body; // Valideret og transformeret
const [result] = await pool.execute(
'INSERT INTO brugere (navn, email, alder) VALUES (?, ?, ?)',
[navn, email, alder]
);
res.status(201).json({ id: result.insertId });
});
module.exports = router;
Trin 7: Input-validering med express-validator
express-validator er alternativet til Zod for Express.js-projekter. Det tilbyder en kæde-baseret API der er tæt integreret med Express middleware-mønsteret og er et godt valg, hvis du allerede har et eksisterende Express-projekt.
// src/routes/produkt-regler.js
const { body, param, query, validationResult } = require('express-validator');
// Valideringsregler som middleware-arrays
const søgRegler = [
query('q')
.trim()
.notEmpty().withMessage('Søgeterm er påkrævet')
.isLength({ min: 2, max: 100 }).withMessage('Søgeterm skal være 2-100 tegn')
.matches(/^[a-zA-ZæøåÆØÅ0-9\s\-\.]+$/).withMessage('Ugyldige tegn i søgeterm'),
];
const idRegler = [
param('id')
.isInt({ min: 1, max: 2147483647 }).withMessage('ID skal være et positivt heltal')
.toInt(),
];
const produktRegler = [
body('navn')
.trim()
.notEmpty().withMessage('Navn er påkrævet')
.isLength({ min: 2, max: 200 }).withMessage('Navn skal være 2-200 tegn'),
body('pris')
.isFloat({ min: 0.01, max: 9999999.99 }).withMessage('Pris skal være et positivt tal')
.toFloat(),
body('kategori')
.trim()
.notEmpty().withMessage('Kategori er påkrævet')
.isIn(['elektronik', 'tøj', 'mad', 'sport']).withMessage('Ugyldig kategori'),
];
// Middleware til at håndtere valideringsfejl
function tjekFejl(req, res, next) {
const fejl = validationResult(req);
if (!fejl.isEmpty()) {
return res.status(400).json({
fejl: 'Valideringsfejl',
detaljer: fejl.array().map((e) => ({
felt: e.path,
besked: e.msg,
})),
});
}
next();
}
module.exports = { søgRegler, idRegler, produktRegler, tjekFejl };
// Brug i route - alle tre middleware kædes
// router.post('/produkt', ...produktRegler, tjekFejl, håndterProdukt)
Trin 8: Mindste privilegium for databasebrugeren
Selv med perfekte parameteriserede forespørgsler skal du begrænse skaden, hvis et angreb lykkes. Mindste privilegium-princippet betyder, at din applikationsbruger kun har de databaserettigheder, der er strengt nødvendige. En angriber der eksekuterer SQL via din applikation kan kun gøre det, som din databasebruger har tilladelse til.
-- Kør disse kommandoer som MySQL root
-- Opret applikationsbruger med stærkt password
CREATE USER 'app_user'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'StærktPassword2026!';
-- Giv kun de nødvendige rettigheder til specifikke tabeller
GRANT SELECT, INSERT, UPDATE ON node_sikker_db.brugere TO 'app_user'@'localhost';
GRANT SELECT, INSERT, UPDATE ON node_sikker_db.produkter TO 'app_user'@'localhost';
GRANT SELECT ON node_sikker_db.kategorier TO 'app_user'@'localhost';
-- Ingen DROP, DELETE på følsomme tabeller - implementer soft delete i stedet
-- Ingen GRANT OPTION - brugeren kan ikke give rettigheder videre
-- Ingen FILE-rettighed - forhindrer filsystem-adgang via SQL
FLUSH PRIVILEGES;
-- Verificer rettigheder
SHOW GRANTS FOR 'app_user'@'localhost';
For PostgreSQL:
-- PostgreSQL mindste privilegium
CREATE USER app_pg_user WITH PASSWORD 'StærktPgPassword2026!';
GRANT CONNECT ON DATABASE node_sikker_pg TO app_pg_user;
GRANT USAGE ON SCHEMA public TO app_pg_user;
GRANT SELECT, INSERT, UPDATE ON TABLE brugere TO app_pg_user;
GRANT SELECT, INSERT, UPDATE ON TABLE produkter TO app_pg_user;
GRANT SELECT ON TABLE kategorier TO app_pg_user;
-- Giv adgang til sequences (påkrævet for INSERT med autoincrement)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_pg_user;
-- Verificer
\dp brugere
Trin 9: Deaktiver multiple statements
Multiple statements-funktionen tillader at sende flere SQL-kommandoer adskilt af semikolon i en enkelt forespørgsel. Det er præcis det, der muliggør angreb som '; DROP TABLE brugere; --. mysql2 deaktiverer dette som standard, men du skal eksplicit bekræfte det i din konfiguration og aldrig overskrive det.
// Eksplicit deaktivering i mysql2 pool-konfiguration
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// KRITISK SIKKERHEDSINDSTILLING - lad denne stå som false
multipleStatements: false,
// Begræns forbindelsestimeout
connectTimeout: 10000,
// Aktiver SSL i produktion
ssl: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: true,
} : undefined,
});
// Kør denne test ved opstart for at bekræfte konfigurationen
async function testSikkerhedKonfiguration() {
try {
await pool.execute('SELECT 1; SELECT 2');
console.error('ADVARSEL: Multiple statements er aktiveret - ret dette med det samme!');
process.exit(1);
} catch (err) {
console.log('OK: Multiple statements er korrekt deaktiveret');
}
}
testSikkerhedKonfiguration();
Trin 10: Allowlisting til dynamiske SQL-strukturer
Kolonnenavne, tabelnavne og sorteringsretninger kan ikke parameteriseres. En angriber der kan styre en ORDER BY-klausul injicerer UNION-angreb eller forårsager fejl der afslører databasestrukturen. Løsningen er allowlisting: du definerer præcist hvilke værdier der er gyldige, og afviser alt andet.
// src/middleware/allowlist.js
const TILLADTE_SORTERINGS_KOLONNER = {
brugere: ['navn', 'email', 'oprettet'],
produkter: ['navn', 'pris', 'kategori', 'oprettet'],
};
const TILLADTE_RETNINGER = ['ASC', 'DESC'];
function validerSortering(tabel, kolonne, retning) {
const tilladeKolonner = TILLADTE_SORTERINGS_KOLONNER[tabel];
if (!tilladeKolonner) {
throw new Error(`Ukendt tabel: ${tabel}`);
}
if (!tilladeKolonner.includes(kolonne)) {
throw new Error(`Ikke-tilladt sorteringskolonne: ${kolonne}`);
}
const normalRetning = retning.toUpperCase();
if (!TILLADTE_RETNINGER.includes(normalRetning)) {
throw new Error(`Ikke-tilladt sorteringsretning: ${retning}`);
}
return { kolonne, retning: normalRetning };
}
// Brug i route
router.get('/produkter', async (req, res) => {
const { sortBy = 'navn', retning = 'ASC' } = req.query;
try {
// Valider mod allowlist FØR brug i SQL
const { kolonne, retning: sikkerRetning } = validerSortering(
'produkter',
sortBy,
retning
);
// Nu er det sikkert at indsætte direkte - de er bekræftet fra kontrolleret liste
const [rows] = await pool.execute(
`SELECT id, navn, pris FROM produkter ORDER BY ${kolonne} ${sikkerRetning}`,
[]
);
res.json(rows);
} catch (err) {
if (err.message.startsWith('Ikke-tilladt') || err.message.startsWith('Ukendt')) {
return res.status(400).json({ fejl: err.message });
}
console.error('Databasefejl:', err.message);
res.status(500).json({ fejl: 'Intern serverfejl' });
}
});
module.exports = { validerSortering };
Trin 11: Fejlhåndtering der ikke lækker SQL-fejl
En SQL-fejlmeddelelse som You have an error in your SQL syntax near '' at line 1 fortæller en angriber, at injection-forsøget nåede databasen. Din fejlhåndtering skal logge detaljerne internt og kun returnere generiske beskeder til klienten.
// src/middleware/fejlhåndtering.js
function globalFejlhåndterer(err, req, res, next) {
const fejlId = Math.random().toString(36).substring(2, 10);
// Log den fulde fejl internt - aldrig til klienten
console.error({
fejlId,
timestamp: new Date().toISOString(),
metode: req.method,
sti: req.path,
fejltype: err.constructor.name,
besked: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
// MySQL fejlkoder starter med ER_
if (err.code && err.code.startsWith('ER_')) {
return res.status(500).json({
fejl: 'Databaseoperation mislykkedes',
fejlId,
});
}
// PostgreSQL unique constraint violation
if (err.code && err.code === '23505') {
return res.status(409).json({
fejl: 'Ressourcen eksisterer allerede',
});
}
res.status(err.status || 500).json({
fejl: process.env.NODE_ENV === 'production'
? 'Intern serverfejl'
: err.message,
fejlId,
});
}
module.exports = globalFejlhåndterer;
Trin 12: Test din applikation med sqlmap
sqlmap er det mest brugte open source-værktøj til at opdage SQL injection-sårbarheder. Kør det mod din lokale udviklingsserver for at verificere, at dine forsvar holder. Brug det kun mod dine egne applikationer eller med eksplicit skriftlig tilladelse fra ejeren.
# Installer sqlmap
pip3 install sqlmap
# Start din Node.js testserver lokalt
node src/app.js &
# Test GET-endpoint mod søge-parameter
sqlmap -u "http://localhost:3000/api/brugere/søg?q=test" \
--level=3 \
--risk=2 \
--batch \
--random-agent
# Forventet output fra en korrekt sikret applikation:
# [INFO] GET parameter 'q' does not seem to be injectable
# [INFO] all tested parameters do not appear to be injectable
# Test POST-endpoint med JSON-body
sqlmap -u "http://localhost:3000/api/produkter" \
--data='{"navn":"test","pris":10,"kategori":"elektronik"}' \
--content-type="application/json" \
--level=3 \
--batch
# Stop testserver
kill %1
Kør sqlmap som en del af din CI/CD-pipeline mod et stagingmiljø ved hvert deployment. Det tager typisk 2-5 minutter for en enkel API og giver maskinel verifikation af dine forsvar.
8 hyppige fejl der åbner for SQL injection
Selv erfarne udviklere begår disse fejl. Kend dem, så du kan opdage dem under code reviews og pull request-gennemgang.
| # | Fejl | Sårbar kode | Sikker løsning |
|---|---|---|---|
| 1 | Template literals med brugerinput | `WHERE id = ${id}` | WHERE id = ? med parameter-array |
| 2 | String-sammensætning | "WHERE navn = '" + navn + "'" | Parameteriserede forespørgsler |
| 3 | Dynamisk ORDER BY uden allowlist | `ORDER BY ${req.query.sort}` | Valider mod foruddefineret liste |
| 4 | Escaped input i stedet for parameterisering | mysql.escape(input) i SQL-streng | Brug ?-pladsholdere konsekvent |
| 5 | Databasefejl eksponeret til klient | res.json({ fejl: err.message }) | Log internt, returner generisk besked |
| 6 | Prisma raw query med brugerinput | prisma.$queryRawUnsafe(sql) | Brug prisma.$queryRaw`...` syntax |
| 7 | Ingen validering af datatyper | Accepter alle strenge som ID | Valider at ID er positivt heltal med Zod |
| 8 | Multiple statements aktiveret | multipleStatements: true | Altid multipleStatements: false |
Avancerede teknikker
Stored procedures som ekstra isolationslag
Stored procedures isolerer SQL-logik i databasen og reducerer risikoen for, at applikationskoden introducerer injection-sårbarheder. Applikationen kalder kun procedurenavnet med parametre og ser aldrig den underliggende SQL. Det er særligt nyttigt, når applikationsudviklerne og databaseadministratorerne er separate teams.
-- Opret stored procedure i MySQL
DELIMITER //
CREATE PROCEDURE HentBruger(IN p_id INT)
BEGIN
SELECT id, navn, email, oprettet
FROM brugere
WHERE id = p_id;
END //
DELIMITER ;
-- Giv applikationsbruger kun EXECUTE-rettighed (ikke direkte tabeladgang)
GRANT EXECUTE ON PROCEDURE node_sikker_db.HentBruger TO 'app_user'@'localhost';
REVOKE SELECT ON node_sikker_db.brugere FROM 'app_user'@'localhost';
// Kald stored procedure fra Node.js
router.get('/bruger/:id', async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id) || id < 1) {
return res.status(400).json({ fejl: 'Ugyldigt ID' });
}
const [rows] = await pool.execute('CALL HentBruger(?)', [id]);
const bruger = rows[0][0]; // Stored procedures returnerer nested array
if (!bruger) {
return res.status(404).json({ fejl: 'Bruger ikke fundet' });
}
res.json(bruger);
});
Runtime-beskyttelse med Aikido Firewall
Aikido Firewall er en open source agent-baseret sikkerhedsplatform til Node.js der blokerer SQL injection-angreb i realtid. Det fungerer som et ekstra netværksniveau under din applikation og fanger angreb der slipper igennem, selv hvis koden indeholder fejl. Det er ikke en erstatning for korrekt parameterisering, men et sikkerhedsnet.
npm install @aikidosec/firewall
// KRITISK: Importer Aikido FØR alle andre moduler på første linje
require('@aikidosec/firewall');
const express = require('express');
// ... resten af din applikation
Automatiseret sikkerhedstjek i CI/CD
SQL injection introduceres også via kompromitterede npm-pakker. Kør automatiske sikkerhedstjek ved hvert commit med npm audit og Snyk:
# .github/workflows/sikkerhed.yml
name: Sikkerhedstjek
on: [push, pull_request]
jobs:
sikkerhed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer Node.js 22
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Installer afhængigheder
run: npm ci
- name: npm audit - fejl ved høj sværhedsgrad
run: npm audit --audit-level=high
- name: Snyk sikkerhedstjek
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
Komplet arbejdende projekt
Her er den fulde src/app.js der samler alle teknikker fra guiden til en produktionsklar applikation:
// src/app.js - Komplet sikker Node.js API mod SQL injection
require('@aikidosec/firewall'); // Altid første linje
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const sikkerBrugereRouter = require('./routes/sikker-brugere');
const produkterRouter = require('./routes/produkter');
const globalFejlhåndterer = require('./middleware/fejlhåndtering');
const app = express();
// Sikkerhedsheadere (CSP, HSTS, X-Frame-Options, osv.)
app.use(helmet());
// Body parsing med størrelsesbegrænsning mod DoS
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// Routes
app.use('/api/brugere', sikkerBrugereRouter);
app.use('/api/produkter', produkterRouter);
// 404 håndtering
app.use((req, res) => {
res.status(404).json({ fejl: 'Ressource ikke fundet' });
});
// Global fejlhåndtering - altid sidst
app.use(globalFejlhåndterer);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Sikker server kører på port ${PORT}`);
});
module.exports = app;
Opsæt testdatabasen og kør projektet:
# Sæt testdatabasetabeller op (MySQL)
mysql -u root -p << 'ENDSQL'
CREATE DATABASE IF NOT EXISTS node_sikker_db;
USE node_sikker_db;
CREATE TABLE IF NOT EXISTS brugere (
id INT AUTO_INCREMENT PRIMARY KEY,
navn VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
alder INT,
oprettet DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS produkter (
id INT AUTO_INCREMENT PRIMARY KEY,
navn VARCHAR(200) NOT NULL,
pris DECIMAL(10,2) NOT NULL,
kategori VARCHAR(50) NOT NULL,
oprettet DATETIME DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO brugere (navn, email, alder) VALUES
('Anders Hansen', '[email protected]', 32),
('Marie Nielsen', '[email protected]', 28);
ENDSQL
# Start applikationen
node src/app.js
# Test sikker søgning
curl "http://localhost:3000/api/brugere/søg?q=Anders"
# Output: [{"id":1,"navn":"Anders Hansen","email":"[email protected]"}]
# Test valideringsfejl ved for kort søgeterm
curl "http://localhost:3000/api/brugere/søg?q=A"
# Output: {"fejl":"Valideringsfejl","detaljer":[{"felt":"q","besked":"Søgning skal være mindst 2 tegn"}]}
# Test SQL injection-forsøg - returnerer valideringsfejl, aldrig SQL-fejl
curl "http://localhost:3000/api/brugere/søg?q=' OR '1'='1"
# Output: {"fejl":"Valideringsfejl","detaljer":[{"felt":"q","besked":"Ugyldige tegn i søgning"}]}
Fejlsøgningsguide: 8 typiske problemer
Problem 1: mysql2 kaster "Cannot read properties of undefined (reading 'execute')"
Årsag: Pool ikke oprettet korrekt, eller du importerer mysql i stedet for mysql2.
Løsning: Tjek at du importerer mysql2/promise og at alle miljøvariabler er sat. Kør console.log(pool) for at bekræfte, at pool-objektet eksisterer og er et Pool-objekt.
Problem 2: Prisma kaster "PrismaClientInitializationError"
Årsag: DATABASE_URL miljøvariabel mangler, eller Prisma Client er ikke genereret efter schema-ændring.
Løsning: Kør npx prisma generate efter enhver ændring af schema.prisma. Bekræft at .env indeholder DATABASE_URL=mysql://bruger:password@localhost:3306/dbnavn.
Problem 3: Zod afviser gyldige dansk input med specialtegn
Årsag: Regex-mønstret ekskluderer æ, ø, å eller andre gyldige tegn.
Løsning: Test regex separat med node -e "console.log(/^[a-zA-ZæøåÆØÅ\s\-']+$/.test('Søren Ødegaard'))". Tilføj manglende tegn til mønstret, herunder bindestreg og apostrof til navne.
Problem 4: sqlmap rapporterer ingen injection trods sårbar kode
Årsag: sqlmap tester standardmæssigt på niveau 1. Komplekse injection-punkter kræver højere niveau.
Løsning: Kør med --level=5 --risk=3 for aggressiv testning. Supplement med manuelle tests: send ' OR '1'='1 direkte og tjek serverloggen.
Problem 5: pg returnerer tom array for forespørgsler der burde give resultater
Årsag: PostgreSQL LIKE er case-sensitiv og matcher ikke store/små bogstaver.
Løsning: Brug ILIKE i stedet for LIKE til case-insensitiv søgning i PostgreSQL: WHERE navn ILIKE $1.
Problem 6: Stored procedure kalder fejler med "PROCEDURE does not exist"
Årsag: Applikationsbrugeren har ikke EXECUTE-rettighed til proceduren, eller proceduren er oprettet i en anden database.
Løsning: Kør GRANT EXECUTE ON PROCEDURE node_sikker_db.HentBruger TO 'app_user'@'localhost'; FLUSH PRIVILEGES;
Problem 7: express-validator fejl vises ikke i respons selvom data er ugyldige
Årsag: tjekFejl-middleware er ikke tilføjet til route-kæden efter valideringsreglerne.
Løsning: Husk altid at inkludere tjekFejl mellem regler og handler: router.post('/sti', [...produktRegler], tjekFejl, handleProdukt).
Problem 8: Aikido Firewall blokerer legitime forespørgsler med SQL-lignende indhold
Årsag: Firewall opdager et mønster der ligner injection i lovlig data, f.eks. SQL-kodeeksempler i en blog-database.
Løsning: Konfigurer Aikido til at tillade specifikke endpoints via AIKIDO_BLOCK=false miljøvariabel for det specifikke endpoint, eller brug Aikido-dashboardet til at justere regler.
Relateret dækning
Yderligere Node.js sikkerhedsguides på shattered.io
- CSRF Protection i Node.js: 12 trin [2026]
- Rate Limiting i Node.js: 12 trin på 30 min [2026]
- Content Security Policy i Node.js: 12 trin [2026]
- JWT Authentication i Node.js: 10 trin [2026]
- Node.js Session Management: 11 trin [2026]
- Sikker session i Node.js: 12 trin på 30 min [2026]
- HMAC i Node.js: webhook-signaturer i 12 trin [2026]
FAQ: SQL Injection i Node.js
Er parameteriserede forespørgsler nok til at stoppe SQL injection?
Parameteriserede forespørgsler er den primære og mest effektive beskyttelse mod SQL injection. De stopper angreb ved at adskille SQL-kode fra brugerdata på protokolniveau. Kombineret med input-validering og mindste privilegium giver de forsvar med 3 uafhængige lag, og alle tre skal være på plads i en produktionsapplikation.
Kan jeg bruge string escaping i stedet for parameterisering?
Nej. String escaping som mysql.escape() er fejlbehæftet og afhænger af korrekt tegnsætkonfiguration. En forkert konfigureret databaseforbindelse kan omgå escaping. Parameteriserede forespørgsler virker på protokolniveau og er ikke afhængige af tegnsæt. Brug altid parameterisering.
Beskytter Prisma ORM automatisk mod SQL injection?
Ja, Prismas standard query-API parameteriserer automatisk alle værdier. Den eneste undtagelse er prisma.$queryRawUnsafe(), som aldrig må bruges med brugerinput. Brug i stedet prisma.$queryRaw`...` med tagged template literals, der parameteriserer sikkert.
Hvad er forskellen på mysql2 .query() og .execute()?
pool.query() sender forespørgslen og parametre i én pakke. pool.execute() bruger prepared statements på MySQL-protokolniveau: SQL-teksten sendes og kompileres separat, derefter sendes parametre. Prepared statements er det stærkeste forsvar og giver bedre ydeevne ved gentagne forespørgsler. Brug altid execute() med brugerinput.
Kan NoSQL-databaser som MongoDB også rammes af injection?
Ja. MongoDB er sårbar over for NoSQL injection, hvor angribere manipulerer query-objekter med operatorer som $where og $gt. Brug mongoose med schema-validering eller sanitiseringsbiblioteker som mongo-sanitize. Principperne om input-validering og mindste privilegium fra denne guide gælder for alle databasetyper.
Skal jeg køre sqlmap i produktion?
Nej. Kør sqlmap kun mod dine egne udviklingsmiljøer eller dedikerede testmiljøer. sqlmap sender aggressive angrebspayloads der kan overbelaste databaser og generere store mængder falske log-alarmer. I produktion bruger du passive overvågningsværktøjer som Aikido Firewall og centraliseret log-analyse til at opdage angrebsforsøg.
Hvad gør jeg, hvis jeg opdager SQL injection i eksisterende produktionskode?
Behandl det som en sikkerhedshændelse. Tjek omgående logs for tegn på udnyttelse. Implementer en midlertidig WAF-regel eller rate limit på det sårbare endpoint. Ret koden: udskift streng-sammensætning med parameteriserede forespørgsler. Deploy via din normale release-proces. Kør sqlmap mod det rettede endpoint for at verificere. Dokumenter hændelsen.
Virker disse teknikker med TypeScript?
Ja, alle pakker i guiden har fulde TypeScript-definitioner. Prisma er bygget TypeScript-first og genererer typer direkte fra dit schema. Zod er TypeScript-first og giver fuldt type-inferens. mysql2 og pg inkluderer respektive typer. TypeScript hjælper med at forhindre SQL injection, fordi streng type-kontrol gør det sværere at konvertere objekter til strenge i SQL-forespørgsler utilsigtet.
Vil du styrke din Node.js-applikation yderligere? Læs OWASP Top 10 for en oversigt over de mest kritiske webapplikationssårbarheder, og Node.js officielle sikkerhedsguide for platformsspecifikke anbefalinger. Snyk dokumenterer løbende opdateringer om SQL injection-forsvar i Node.js, og Prisma dokumenterer sikker brug af raw queries i detaljer. StackHawk udgav i 2025 en teknisk guide til Node.js SQL injection-eksempler og forebyggelse.




