{"id":121,"date":"2026-06-18T16:37:30","date_gmt":"2026-06-18T16:37:30","guid":{"rendered":"https:\/\/shattered.io\/dk\/2026\/06\/18\/sql-injection-nodejs\/"},"modified":"2026-06-18T16:38:58","modified_gmt":"2026-06-18T16:38:58","slug":"sql-injection-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/dk\/sql-injection-nodejs\/","title":{"rendered":"SQL Injection i Node.js: 12 trin til sikker database [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">SQL injection er stadig en af de mest udbredte s\u00e5rbarheder i webapplikationer i 2026. En enkelt usikker databaseforesp\u00f8rgsel giver angribere adgang til at l\u00e6se, \u00e6ndre eller slette hele din database uden at kende et eneste password. OWASP placerer injection-angreb som <strong>A03 i Top 10<\/strong> over de farligste webapplikationss\u00e5rbarheder, og Node.js-applikationer er ikke immune. En analyse fra Stackademic i januar 2026 dokumenterer, at SQL injection stadig ses overalt i produktionskode, p\u00e5 trods af at l\u00f8sningerne er veldokumenterede og tilg\u00e6ngelige.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Denne guide viser dig pr\u00e6cis, hvordan du eliminerer SQL injection i Node.js trin for trin. Du l\u00e6rer at bruge parameteriserede foresp\u00f8rgsler med mysql2 og pg, sikre ORMs som Prisma, inputvalidering med Zod og express-validator, og en r\u00e6kke avancerede teknikker der beskytter din applikation i produktionsmilj\u00f8et. Guiden er baseret p\u00e5 research fra 2025-2026 og d\u00e6kker den kode, du skriver i dag.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hvad-er-sql-injection-og-hvorfor-er-det-farligt-i-2026\">Hvad er SQL injection, og hvorfor er det farligt i 2026?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SQL injection opst\u00e5r, n\u00e5r brugerinput flettes direkte ind i en SQL-foresp\u00f8rgsel som tekst. Databasemotoren kan ikke skelne mellem foresp\u00f8rgselslogik og brugerdata, og en angriber kan dermed injicere sin egen SQL-kode. Konsekvenserne sp\u00e6nder fra datal\u00e6k til fuldst\u00e6ndig overtagelse af serveren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tag dette Node.js-eksempel, der <strong>aldrig<\/strong> m\u00e5 bruges i produktion:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ FARLIGT - Brug aldrig dette m\u00f8nster\nconst username = req.body.username;\nconst query = `SELECT * FROM users WHERE username = '${username}'`;\nconnection.query(query, (err, results) => {\n  \/\/ En angriber sender: admin' OR '1'='1\n  \/\/ SQL bliver: SELECT * FROM users WHERE username = 'admin' OR '1'='1'\n  \/\/ Resultat: Alle brugere returneres\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En angriber indsender <code>admin' OR '1'='1<\/code> som brugernavn, og foresp\u00f8rgslen returnerer alle brugere i databasen. Med et lidt mere avanceret payload som <code>'; DROP TABLE users; --<\/code> sletter angriberen hele tabellen.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Angrebstype<\/th><th>Eksempel-payload<\/th><th>Effekt<\/th><th>L\u00f8sning<\/th><\/tr><\/thead><tbody><tr><td>Classic injection<\/td><td><code>' OR '1'='1<\/code><\/td><td>Omg\u00e5 login<\/td><td>Parameteriserede queries<\/td><\/tr><tr><td>UNION-baseret<\/td><td><code>' UNION SELECT password FROM users--<\/code><\/td><td>Dataudtr\u00e6k<\/td><td>Parameteriserede queries og ORM<\/td><\/tr><tr><td>Blind (boolean)<\/td><td><code>' AND 1=1--<\/code><\/td><td>Skjult dataudtr\u00e6k<\/td><td>Input-validering og WAF<\/td><\/tr><tr><td>Time-based blind<\/td><td><code>'; WAITFOR DELAY '0:0:5'--<\/code><\/td><td>Bekr\u00e6ft s\u00e5rbarhed<\/td><td>Query-timeout og parameterisering<\/td><\/tr><tr><td>Stacked queries<\/td><td><code>'; INSERT INTO admin VALUES(...)--<\/code><\/td><td>Datamanipulation<\/td><td>Deaktiver multiple statements<\/td><\/tr><tr><td>Out-of-band<\/td><td><code>' EXEC xp_cmdshell('ping attacker.com')--<\/code><\/td><td>Kommandoudf\u00f8relse<\/td><td>Mindste privilegium og WAF<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"forudsaetninger-og-versioner\">Foruds\u00e6tninger og versioner<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Inden du begynder, skal du have f\u00f8lgende installeret og konfigureret:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Node.js 22.x LTS<\/strong> eller nyere (aktiv support i 2026)<\/li>\n<li><strong>npm 10.x<\/strong> eller nyere<\/li>\n<li><strong>MySQL 8.x<\/strong> eller <strong>PostgreSQL 16.x<\/strong> (eksempler d\u00e6kker begge)<\/li>\n<li>Grundl\u00e6ggende kendskab til Express.js og async\/await<\/li>\n<li>En lokal MySQL\/PostgreSQL-installation til testform\u00e5l<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Pakker du installerer i denne guide:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Pakke<\/th><th>Form\u00e5l<\/th><th>Installation<\/th><\/tr><\/thead><tbody><tr><td>express<\/td><td>HTTP-server<\/td><td><code>npm install express<\/code><\/td><\/tr><tr><td>mysql2<\/td><td>MySQL-driver med parameterisering<\/td><td><code>npm install mysql2<\/code><\/td><\/tr><tr><td>pg<\/td><td>PostgreSQL-driver<\/td><td><code>npm install pg<\/code><\/td><\/tr><tr><td>@prisma\/client<\/td><td>Type-sikker ORM<\/td><td><code>npm install @prisma\/client<\/code><\/td><\/tr><tr><td>prisma<\/td><td>Prisma CLI (dev-afh\u00e6ngighed)<\/td><td><code>npm install --save-dev prisma<\/code><\/td><\/tr><tr><td>zod<\/td><td>Schema-validering<\/td><td><code>npm install zod<\/code><\/td><\/tr><tr><td>express-validator<\/td><td>Express middleware-validering<\/td><td><code>npm install express-validator<\/code><\/td><\/tr><tr><td>dotenv<\/td><td>Milj\u00f8variabler<\/td><td><code>npm install dotenv<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-1-projektopsaetning\">Trin 1: Projektops\u00e6tning<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start med at oprette et rent projekt med den korrekte mappestruktur. En god struktur g\u00f8r det nemmere at h\u00e5ndh\u00e6ve sikkerhedsregler konsekvent p\u00e5 tv\u00e6rs af al kode.<\/p>\n\n\n\n<pre><code class=\"language-bash\">mkdir node-sql-sikker && cd node-sql-sikker\nnpm init -y\nnpm install express mysql2 pg @prisma\/client zod express-validator dotenv\nnpm install --save-dev prisma nodemon\n\n# Opret projektstruktur\nmkdir -p src\/{routes,middleware,db,validators}\ntouch src\/app.js src\/db\/mysql.js src\/db\/postgres.js .env<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tilf\u00f8j f\u00f8lgende til din <code>.env<\/code>-fil. Commit <strong>aldrig<\/strong> denne fil til versionsstyring:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># .env\nDB_HOST=localhost\nDB_PORT=3306\nDB_USER=app_user          # Brug IKKE root\nDB_PASSWORD=dit_st\u00e6rke_password\nDB_NAME=node_sikker_db\nDB_POOL_MAX=10\n\nPG_HOST=localhost\nPG_PORT=5432\nPG_USER=app_pg_user\nPG_PASSWORD=dit_postgres_password\nPG_DATABASE=node_sikker_pg<\/code><\/pre>\n\n\n\n<pre><code class=\"language-bash\">echo \".env\" >> .gitignore\necho \"node_modules\/\" >> .gitignore<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-2-forstaa-den-saarbare-kode\">Trin 2: Forst\u00e5 den s\u00e5rbare kode<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Inden du retter et problem, skal du forst\u00e5 det til bunds. Her er tre typiske s\u00e5rbare m\u00f8nstre du ofte ser i Node.js-kode, og som du skal genkende med det samme:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ FARLIGE M\u00d8NSTRE - Vis kun til undervisningsform\u00e5l\n\n\/\/ M\u00f8nster 1: Template literals med brugerinput\nconst s\u00f8g = req.query.q;\ndb.query(`SELECT * FROM produkter WHERE navn LIKE '%${s\u00f8g}%'`);\n\n\/\/ M\u00f8nster 2: String-sammens\u00e6tning\nconst id = req.params.id;\ndb.query(\"SELECT * FROM brugere WHERE id = \" + id);\n\n\/\/ M\u00f8nster 3: Ufiltreret sortering (kan ikke parameteriseres)\nconst kolonne = req.query.sortBy;\ndb.query(`SELECT * FROM ordrer ORDER BY ${kolonne} ASC`);\n\/\/ Angriber sender: kolonne = \"1; DROP TABLE ordrer; --\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Det tredje m\u00f8nster er s\u00e6rligt sv\u00e6rt at opdage, fordi kolonnenavne ikke kan parameteriseres direkte. Du skal i stedet bruge allowlisting (trin 10) til sorteringskolonner og andre dynamiske strukturer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-3-parameteriserede-forespoergsler-med-mysql2\">Trin 3: Parameteriserede foresp\u00f8rgsler med mysql2<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">mysql2-pakken er den anbefalede MySQL-driver til Node.js i 2026. Den underst\u00f8tter parameteriserede foresp\u00f8rgsler som standard og bruger <code>?<\/code>-pladsholdere. V\u00e6rdier sendes som et separat array, aldrig flettet ind i SQL-strengen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Opret din database-forbindelsesops\u00e6tning i <code>src\/db\/mysql.js<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/db\/mysql.js\nrequire('dotenv').config();\nconst mysql = require('mysql2\/promise');\n\nconst pool = mysql.createPool({\n  host: process.env.DB_HOST,\n  port: parseInt(process.env.DB_PORT),\n  user: process.env.DB_USER,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n  connectionLimit: parseInt(process.env.DB_POOL_MAX),\n  multipleStatements: false,  \/\/ KRITISK: deaktiver multiple statements\n  waitForConnections: true,\n  queueLimit: 0,\n});\n\nmodule.exports = pool;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Brug nu parameteriserede foresp\u00f8rgsler i alle dine routes:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/routes\/brugere.js\nconst express = require('express');\nconst pool = require('..\/db\/mysql');\nconst router = express.Router();\n\n\/\/ SIKKERT: Parameteriseret foresp\u00f8rgsel med ?-pladsholder\nrouter.get('\/bruger\/:id', async (req, res) => {\n  const { id } = req.params;\n\n  try {\n    \/\/ id inds\u00e6ttes som parameter, ikke i SQL-strengen\n    const [rows] = await pool.execute(\n      'SELECT id, navn, email FROM brugere WHERE id = ?',\n      [id]\n    );\n\n    if (rows.length === 0) {\n      return res.status(404).json({ fejl: 'Bruger ikke fundet' });\n    }\n\n    res.json(rows[0]);\n  } catch (err) {\n    \/\/ Afsl\u00f8r ALDRIG databasefejl til klienten\n    console.error('Databasefejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});\n\n\/\/ SIKKERT: S\u00f8gning med LIKE\nrouter.get('\/s\u00f8g', async (req, res) => {\n  const { q } = req.query;\n\n  try {\n    \/\/ Indpak wildcards p\u00e5 serversiden, ikke klientsiden\n    const s\u00f8gterm = `%${q}%`;\n    const [rows] = await pool.execute(\n      'SELECT id, navn, email FROM brugere WHERE navn LIKE ?',\n      [s\u00f8gterm]\n    );\n    res.json(rows);\n  } catch (err) {\n    console.error('S\u00f8gningsfejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Bem\u00e6rk forskellen p\u00e5 <code>pool.query()<\/code> og <code>pool.execute()<\/code>: <code>execute()<\/code> bruger prepared statements p\u00e5 protokolniveau, hvilket giver det st\u00e6rkeste forsvar. Brug altid <code>execute()<\/code> med brugerinput.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-4-parameteriserede-forespoergsler-med-pg-postgresql\">Trin 4: Parameteriserede foresp\u00f8rgsler med pg (PostgreSQL)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PostgreSQL bruger <code>$1, $2, $3<\/code>-pladsholdere i stedet for <code>?<\/code>. pg-pakken h\u00e5ndterer parameterisering korrekt, n\u00e5r du adskiller SQL-tekst fra parametre.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/db\/postgres.js\nrequire('dotenv').config();\nconst { Pool } = require('pg');\n\nconst pgPool = new Pool({\n  host: process.env.PG_HOST,\n  port: parseInt(process.env.PG_PORT),\n  user: process.env.PG_USER,\n  password: process.env.PG_PASSWORD,\n  database: process.env.PG_DATABASE,\n  max: 10,\n  idleTimeoutMillis: 30000,\n  connectionTimeoutMillis: 2000,\n});\n\nmodule.exports = pgPool;\n\n\/\/ src\/routes\/produkter.js\nconst pgPool = require('..\/db\/postgres');\nconst router = require('express').Router();\n\nrouter.get('\/produkt\/:id', async (req, res) => {\n  const { id } = req.params;\n\n  try {\n    \/\/ PostgreSQL-parameterisering med $1-pladsholder\n    const result = await pgPool.query(\n      'SELECT id, navn, pris, kategori FROM produkter WHERE id = $1',\n      [id]\n    );\n\n    if (result.rows.length === 0) {\n      return res.status(404).json({ fejl: 'Produkt ikke fundet' });\n    }\n\n    res.json(result.rows[0]);\n  } catch (err) {\n    console.error('PostgreSQL-fejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});\n\n\/\/ Inds\u00e6t data sikkert med flere parametre\nrouter.post('\/produkt', async (req, res) => {\n  const { navn, pris, kategori } = req.body;\n\n  try {\n    const result = await pgPool.query(\n      'INSERT INTO produkter (navn, pris, kategori) VALUES ($1, $2, $3) RETURNING id',\n      [navn, pris, kategori]\n    );\n\n    res.status(201).json({ id: result.rows[0].id });\n  } catch (err) {\n    console.error('Inds\u00e6tningsfejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-5-prisma-orm-til-automatisk-parameterisering\">Trin 5: Prisma ORM til automatisk parameterisering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prisma er en type-sikker ORM der automatisk parameteriserer alle foresp\u00f8rgsler. Du skriver aldrig r\u00e5 SQL i normale operationer, og Prisma h\u00e5ndterer alle databindinger korrekt. Det er en af de st\u00e6rkeste beskyttelser mod SQL injection, fordi fejlmuligheden elimineres strukturelt i stedet for at afh\u00e6nge af, at udvikleren husker at g\u00f8re det korrekt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ops\u00e6t Prisma til MySQL:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Initialiser Prisma\nnpx prisma init --datasource-provider mysql\n\n# Generer Prisma Client efter schema-\u00e6ndringer\nnpx prisma generate\n\n# Push schema til databasen (kun i udvikling)\nnpx prisma db push<\/code><\/pre>\n\n\n\n<pre><code class=\"language-prisma\">\/\/ prisma\/schema.prisma\ngenerator client {\n  provider = \"prisma-client-js\"\n}\n\ndatasource db {\n  provider = \"mysql\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Bruger {\n  id        Int      @id @default(autoincrement())\n  navn      String   @db.VarChar(100)\n  email     String   @unique @db.VarChar(255)\n  oprettet  DateTime @default(now())\n\n  @@map(\"brugere\")\n}\n\nmodel Produkt {\n  id        Int      @id @default(autoincrement())\n  navn      String   @db.VarChar(200)\n  pris      Decimal  @db.Decimal(10, 2)\n  kategori  String   @db.VarChar(50)\n\n  @@map(\"produkter\")\n}<\/code><\/pre>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/routes\/prisma-brugere.js\nconst { PrismaClient } = require('@prisma\/client');\nconst router = require('express').Router();\nconst prisma = new PrismaClient();\n\n\/\/ SIKKERT: Prisma parameteriserer automatisk via query-API\nrouter.get('\/bruger\/:id', async (req, res) => {\n  const id = parseInt(req.params.id);\n\n  if (isNaN(id)) {\n    return res.status(400).json({ fejl: 'Ugyldigt ID' });\n  }\n\n  try {\n    const bruger = await prisma.bruger.findUnique({\n      where: { id },\n      select: { id: true, navn: true, email: true, oprettet: true }\n    });\n\n    if (!bruger) {\n      return res.status(404).json({ fejl: 'Bruger ikke fundet' });\n    }\n\n    res.json(bruger);\n  } catch (err) {\n    console.error('Prisma-fejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});\n\n\/\/ Sikker s\u00f8gning med Prisma - automatisk escaped\nrouter.get('\/s\u00f8g', async (req, res) => {\n  const { q } = req.query;\n\n  try {\n    const brugere = await prisma.bruger.findMany({\n      where: {\n        navn: {\n          contains: q,\n          mode: 'insensitive'\n        }\n      },\n      select: { id: true, navn: true, email: true }\n    });\n\n    res.json(brugere);\n  } catch (err) {\n    console.error('S\u00f8gningsfejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});\n\n\/\/ Raw SQL med Prisma - brug KUN tagget template literal\nrouter.get('\/avanceret\/:kategori', async (req, res) => {\n  const { kategori } = req.params;\n\n  try {\n    \/\/ prisma.$queryRaw med tagged template er sikker\n    \/\/ Brug ALDRIG prisma.$queryRawUnsafe() med brugerinput\n    const produkter = await prisma.$queryRaw`\n      SELECT id, navn, pris\n      FROM produkter\n      WHERE kategori = ${kategori}\n      ORDER BY pris ASC\n    `;\n\n    res.json(produkter);\n  } catch (err) {\n    console.error('Raw query fejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-6-input-validering-med-zod\">Trin 6: Input-validering med Zod<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Parameteriserede foresp\u00f8rgsler stopper SQL injection p\u00e5 databaseniveau. Input-validering stopper det allerede ved API-gr\u00e6nsen, inden data overhovedet n\u00e5r databaselaget. Disse to lag supplerer hinanden og m\u00e5 ikke erstatte hinanden. Zod er et TypeScript-first schema-valideringsbibliotek der fungerer fremragende med ren JavaScript. Det validerer ikke bare at en v\u00e6rdi er til stede, men kontrollerer type, format, l\u00e6ngde og begr\u00e6nsninger pr\u00e6cist.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/validators\/bruger-validator.js\nconst { z } = require('zod');\n\nconst brugerS\u00f8gSchema = z.object({\n  q: z\n    .string()\n    .min(2, 'S\u00f8gning skal v\u00e6re mindst 2 tegn')\n    .max(100, 'S\u00f8gning m\u00e5 maksimalt v\u00e6re 100 tegn')\n    .regex(\/^[a-zA-Z\u00e6\u00f8\u00e5\u00c6\u00d8\u00c50-9\\s\\-\\.]+$\/, 'Ugyldige tegn i s\u00f8gning'),\n});\n\nconst brugerIdSchema = z.object({\n  id: z\n    .string()\n    .regex(\/^\\d+$\/, 'ID skal v\u00e6re et heltal')\n    .transform(Number)\n    .refine((n) => n > 0 && n < 2147483647, 'ID uden for gyldigt interval'),\n});\n\nconst opretBrugerSchema = z.object({\n  navn: z\n    .string()\n    .min(2, 'Navn skal v\u00e6re mindst 2 tegn')\n    .max(100, 'Navn m\u00e5 maksimalt v\u00e6re 100 tegn')\n    .regex(\/^[a-zA-Z\u00e6\u00f8\u00e5\u00c6\u00d8\u00c5\\s\\-']+$\/, 'Ugyldige tegn i navn'),\n  email: z\n    .string()\n    .email('Ugyldig e-mailadresse')\n    .max(255, 'E-mail er for lang')\n    .toLowerCase(),\n  alder: z\n    .number()\n    .int('Alder skal v\u00e6re et heltal')\n    .min(13, 'Minimum alder er 13')\n    .max(120, 'Ugyldig alder'),\n});\n\nmodule.exports = { brugerS\u00f8gSchema, brugerIdSchema, opretBrugerSchema };\n\n\/\/ src\/middleware\/valider.js\nconst { ZodError } = require('zod');\n\nfunction valider(schema, kilde = 'body') {\n  return (req, res, next) => {\n    try {\n      const data = schema.parse(req[kilde]);\n      req[kilde] = data; \/\/ Udskift med valideret og transformeret data\n      next();\n    } catch (err) {\n      if (err instanceof ZodError) {\n        return res.status(400).json({\n          fejl: 'Valideringsfejl',\n          detaljer: err.errors.map((e) => ({\n            felt: e.path.join('.'),\n            besked: e.message,\n          })),\n        });\n      }\n      next(err);\n    }\n  };\n}\n\nmodule.exports = valider;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Brug middleware i dine routes:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/routes\/sikker-brugere.js\nconst router = require('express').Router();\nconst pool = require('..\/db\/mysql');\nconst valider = require('..\/middleware\/valider');\nconst { brugerS\u00f8gSchema, opretBrugerSchema } = require('..\/validators\/bruger-validator');\n\n\/\/ Valideringsfejl returneres INDEN databasekaldet sker\nrouter.get('\/s\u00f8g', valider(brugerS\u00f8gSchema, 'query'), async (req, res) => {\n  const { q } = req.query; \/\/ Garanteret valideret og renset\n\n  const s\u00f8gterm = `%${q}%`;\n  const [rows] = await pool.execute(\n    'SELECT id, navn, email FROM brugere WHERE navn LIKE ?',\n    [s\u00f8gterm]\n  );\n  res.json(rows);\n});\n\nrouter.post('\/opret', valider(opretBrugerSchema), async (req, res) => {\n  const { navn, email, alder } = req.body; \/\/ Valideret og transformeret\n\n  const [result] = await pool.execute(\n    'INSERT INTO brugere (navn, email, alder) VALUES (?, ?, ?)',\n    [navn, email, alder]\n  );\n\n  res.status(201).json({ id: result.insertId });\n});\n\nmodule.exports = router;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-7-input-validering-med-express-validator\">Trin 7: Input-validering med express-validator<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">express-validator er alternativet til Zod for Express.js-projekter. Det tilbyder en k\u00e6de-baseret API der er t\u00e6t integreret med Express middleware-m\u00f8nsteret og er et godt valg, hvis du allerede har et eksisterende Express-projekt.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/routes\/produkt-regler.js\nconst { body, param, query, validationResult } = require('express-validator');\n\n\/\/ Valideringsregler som middleware-arrays\nconst s\u00f8gRegler = [\n  query('q')\n    .trim()\n    .notEmpty().withMessage('S\u00f8geterm er p\u00e5kr\u00e6vet')\n    .isLength({ min: 2, max: 100 }).withMessage('S\u00f8geterm skal v\u00e6re 2-100 tegn')\n    .matches(\/^[a-zA-Z\u00e6\u00f8\u00e5\u00c6\u00d8\u00c50-9\\s\\-\\.]+$\/).withMessage('Ugyldige tegn i s\u00f8geterm'),\n];\n\nconst idRegler = [\n  param('id')\n    .isInt({ min: 1, max: 2147483647 }).withMessage('ID skal v\u00e6re et positivt heltal')\n    .toInt(),\n];\n\nconst produktRegler = [\n  body('navn')\n    .trim()\n    .notEmpty().withMessage('Navn er p\u00e5kr\u00e6vet')\n    .isLength({ min: 2, max: 200 }).withMessage('Navn skal v\u00e6re 2-200 tegn'),\n  body('pris')\n    .isFloat({ min: 0.01, max: 9999999.99 }).withMessage('Pris skal v\u00e6re et positivt tal')\n    .toFloat(),\n  body('kategori')\n    .trim()\n    .notEmpty().withMessage('Kategori er p\u00e5kr\u00e6vet')\n    .isIn(['elektronik', 't\u00f8j', 'mad', 'sport']).withMessage('Ugyldig kategori'),\n];\n\n\/\/ Middleware til at h\u00e5ndtere valideringsfejl\nfunction tjekFejl(req, res, next) {\n  const fejl = validationResult(req);\n  if (!fejl.isEmpty()) {\n    return res.status(400).json({\n      fejl: 'Valideringsfejl',\n      detaljer: fejl.array().map((e) => ({\n        felt: e.path,\n        besked: e.msg,\n      })),\n    });\n  }\n  next();\n}\n\nmodule.exports = { s\u00f8gRegler, idRegler, produktRegler, tjekFejl };\n\n\/\/ Brug i route - alle tre middleware k\u00e6des\n\/\/ router.post('\/produkt', ...produktRegler, tjekFejl, h\u00e5ndterProdukt)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-8-mindste-privilegium-for-databasebrugeren\">Trin 8: Mindste privilegium for databasebrugeren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Selv med perfekte parameteriserede foresp\u00f8rgsler skal du begr\u00e6nse skaden, hvis et angreb lykkes. Mindste privilegium-princippet betyder, at din applikationsbruger kun har de databaserettigheder, der er strengt n\u00f8dvendige. En angriber der eksekuterer SQL via din applikation kan kun g\u00f8re det, som din databasebruger har tilladelse til.<\/p>\n\n\n\n<pre><code class=\"language-sql\">-- K\u00f8r disse kommandoer som MySQL root\n-- Opret applikationsbruger med st\u00e6rkt password\nCREATE USER 'app_user'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'St\u00e6rktPassword2026!';\n\n-- Giv kun de n\u00f8dvendige rettigheder til specifikke tabeller\nGRANT SELECT, INSERT, UPDATE ON node_sikker_db.brugere TO 'app_user'@'localhost';\nGRANT SELECT, INSERT, UPDATE ON node_sikker_db.produkter TO 'app_user'@'localhost';\nGRANT SELECT ON node_sikker_db.kategorier TO 'app_user'@'localhost';\n\n-- Ingen DROP, DELETE p\u00e5 f\u00f8lsomme tabeller - implementer soft delete i stedet\n-- Ingen GRANT OPTION - brugeren kan ikke give rettigheder videre\n-- Ingen FILE-rettighed - forhindrer filsystem-adgang via SQL\n\nFLUSH PRIVILEGES;\n\n-- Verificer rettigheder\nSHOW GRANTS FOR 'app_user'@'localhost';<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For PostgreSQL:<\/p>\n\n\n\n<pre><code class=\"language-sql\">-- PostgreSQL mindste privilegium\nCREATE USER app_pg_user WITH PASSWORD 'St\u00e6rktPgPassword2026!';\n\nGRANT CONNECT ON DATABASE node_sikker_pg TO app_pg_user;\nGRANT USAGE ON SCHEMA public TO app_pg_user;\nGRANT SELECT, INSERT, UPDATE ON TABLE brugere TO app_pg_user;\nGRANT SELECT, INSERT, UPDATE ON TABLE produkter TO app_pg_user;\nGRANT SELECT ON TABLE kategorier TO app_pg_user;\n\n-- Giv adgang til sequences (p\u00e5kr\u00e6vet for INSERT med autoincrement)\nGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_pg_user;\n\n-- Verificer\n\\dp brugere<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-9-deaktiver-multiple-statements\">Trin 9: Deaktiver multiple statements<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Multiple statements-funktionen tillader at sende flere SQL-kommandoer adskilt af semikolon i en enkelt foresp\u00f8rgsel. Det er pr\u00e6cis det, der muligg\u00f8r angreb som <code>'; DROP TABLE brugere; --<\/code>. mysql2 deaktiverer dette som standard, men du skal eksplicit bekr\u00e6fte det i din konfiguration og aldrig overskrive det.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ Eksplicit deaktivering i mysql2 pool-konfiguration\nconst pool = mysql.createPool({\n  host: process.env.DB_HOST,\n  user: process.env.DB_USER,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n\n  \/\/ KRITISK SIKKERHEDSINDSTILLING - lad denne st\u00e5 som false\n  multipleStatements: false,\n\n  \/\/ Begr\u00e6ns forbindelsestimeout\n  connectTimeout: 10000,\n\n  \/\/ Aktiver SSL i produktion\n  ssl: process.env.NODE_ENV === 'production' ? {\n    rejectUnauthorized: true,\n  } : undefined,\n});\n\n\/\/ K\u00f8r denne test ved opstart for at bekr\u00e6fte konfigurationen\nasync function testSikkerhedKonfiguration() {\n  try {\n    await pool.execute('SELECT 1; SELECT 2');\n    console.error('ADVARSEL: Multiple statements er aktiveret - ret dette med det samme!');\n    process.exit(1);\n  } catch (err) {\n    console.log('OK: Multiple statements er korrekt deaktiveret');\n  }\n}\n\ntestSikkerhedKonfiguration();<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-10-allowlisting-til-dynamiske-sql-strukturer\">Trin 10: Allowlisting til dynamiske SQL-strukturer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Kolonnenavne, tabelnavne og sorteringsretninger kan ikke parameteriseres. En angriber der kan styre en <code>ORDER BY<\/code>-klausul injicerer UNION-angreb eller for\u00e5rsager fejl der afsl\u00f8rer databasestrukturen. L\u00f8sningen er allowlisting: du definerer pr\u00e6cist hvilke v\u00e6rdier der er gyldige, og afviser alt andet.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/middleware\/allowlist.js\n\nconst TILLADTE_SORTERINGS_KOLONNER = {\n  brugere: ['navn', 'email', 'oprettet'],\n  produkter: ['navn', 'pris', 'kategori', 'oprettet'],\n};\n\nconst TILLADTE_RETNINGER = ['ASC', 'DESC'];\n\nfunction validerSortering(tabel, kolonne, retning) {\n  const tilladeKolonner = TILLADTE_SORTERINGS_KOLONNER[tabel];\n\n  if (!tilladeKolonner) {\n    throw new Error(`Ukendt tabel: ${tabel}`);\n  }\n\n  if (!tilladeKolonner.includes(kolonne)) {\n    throw new Error(`Ikke-tilladt sorteringskolonne: ${kolonne}`);\n  }\n\n  const normalRetning = retning.toUpperCase();\n  if (!TILLADTE_RETNINGER.includes(normalRetning)) {\n    throw new Error(`Ikke-tilladt sorteringsretning: ${retning}`);\n  }\n\n  return { kolonne, retning: normalRetning };\n}\n\n\/\/ Brug i route\nrouter.get('\/produkter', async (req, res) => {\n  const { sortBy = 'navn', retning = 'ASC' } = req.query;\n\n  try {\n    \/\/ Valider mod allowlist F\u00d8R brug i SQL\n    const { kolonne, retning: sikkerRetning } = validerSortering(\n      'produkter',\n      sortBy,\n      retning\n    );\n\n    \/\/ Nu er det sikkert at inds\u00e6tte direkte - de er bekr\u00e6ftet fra kontrolleret liste\n    const [rows] = await pool.execute(\n      `SELECT id, navn, pris FROM produkter ORDER BY ${kolonne} ${sikkerRetning}`,\n      []\n    );\n\n    res.json(rows);\n  } catch (err) {\n    if (err.message.startsWith('Ikke-tilladt') || err.message.startsWith('Ukendt')) {\n      return res.status(400).json({ fejl: err.message });\n    }\n    console.error('Databasefejl:', err.message);\n    res.status(500).json({ fejl: 'Intern serverfejl' });\n  }\n});\n\nmodule.exports = { validerSortering };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-11-fejlhaandtering-der-ikke-laekker-sql-fejl\">Trin 11: Fejlh\u00e5ndtering der ikke l\u00e6kker SQL-fejl<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En SQL-fejlmeddelelse som <code>You have an error in your SQL syntax near '' at line 1<\/code> fort\u00e6ller en angriber, at injection-fors\u00f8get n\u00e5ede databasen. Din fejlh\u00e5ndtering skal logge detaljerne internt og kun returnere generiske beskeder til klienten.<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/middleware\/fejlh\u00e5ndtering.js\n\nfunction globalFejlh\u00e5ndterer(err, req, res, next) {\n  const fejlId = Math.random().toString(36).substring(2, 10);\n\n  \/\/ Log den fulde fejl internt - aldrig til klienten\n  console.error({\n    fejlId,\n    timestamp: new Date().toISOString(),\n    metode: req.method,\n    sti: req.path,\n    fejltype: err.constructor.name,\n    besked: err.message,\n    stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,\n  });\n\n  \/\/ MySQL fejlkoder starter med ER_\n  if (err.code && err.code.startsWith('ER_')) {\n    return res.status(500).json({\n      fejl: 'Databaseoperation mislykkedes',\n      fejlId,\n    });\n  }\n\n  \/\/ PostgreSQL unique constraint violation\n  if (err.code && err.code === '23505') {\n    return res.status(409).json({\n      fejl: 'Ressourcen eksisterer allerede',\n    });\n  }\n\n  res.status(err.status || 500).json({\n    fejl: process.env.NODE_ENV === 'production'\n      ? 'Intern serverfejl'\n      : err.message,\n    fejlId,\n  });\n}\n\nmodule.exports = globalFejlh\u00e5ndterer;<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-12-test-din-applikation-med-sqlmap\">Trin 12: Test din applikation med sqlmap<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">sqlmap er det mest brugte open source-v\u00e6rkt\u00f8j til at opdage SQL injection-s\u00e5rbarheder. K\u00f8r det mod din lokale udviklingsserver for at verificere, at dine forsvar holder. Brug det <strong>kun<\/strong> mod dine egne applikationer eller med eksplicit skriftlig tilladelse fra ejeren.<\/p>\n\n\n\n<pre><code class=\"language-bash\"># Installer sqlmap\npip3 install sqlmap\n\n# Start din Node.js testserver lokalt\nnode src\/app.js &\n\n# Test GET-endpoint mod s\u00f8ge-parameter\nsqlmap -u \"http:\/\/localhost:3000\/api\/brugere\/s\u00f8g?q=test\" \\\n  --level=3 \\\n  --risk=2 \\\n  --batch \\\n  --random-agent\n\n# Forventet output fra en korrekt sikret applikation:\n# [INFO] GET parameter 'q' does not seem to be injectable\n# [INFO] all tested parameters do not appear to be injectable\n\n# Test POST-endpoint med JSON-body\nsqlmap -u \"http:\/\/localhost:3000\/api\/produkter\" \\\n  --data='{\"navn\":\"test\",\"pris\":10,\"kategori\":\"elektronik\"}' \\\n  --content-type=\"application\/json\" \\\n  --level=3 \\\n  --batch\n\n# Stop testserver\nkill %1<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r sqlmap som en del af din CI\/CD-pipeline mod et stagingmilj\u00f8 ved hvert deployment. Det tager typisk 2-5 minutter for en enkel API og giver maskinel verifikation af dine forsvar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"8-hyppige-fejl-der-aabner-for-sql-injection\">8 hyppige fejl der \u00e5bner for SQL injection<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Selv erfarne udviklere beg\u00e5r disse fejl. Kend dem, s\u00e5 du kan opdage dem under code reviews og pull request-gennemgang.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>#<\/th><th>Fejl<\/th><th>S\u00e5rbar kode<\/th><th>Sikker l\u00f8sning<\/th><\/tr><\/thead><tbody><tr><td>1<\/td><td>Template literals med brugerinput<\/td><td><code>`WHERE id = ${id}`<\/code><\/td><td><code>WHERE id = ?<\/code> med parameter-array<\/td><\/tr><tr><td>2<\/td><td>String-sammens\u00e6tning<\/td><td><code>\"WHERE navn = '\" + navn + \"'\"<\/code><\/td><td>Parameteriserede foresp\u00f8rgsler<\/td><\/tr><tr><td>3<\/td><td>Dynamisk ORDER BY uden allowlist<\/td><td><code>`ORDER BY ${req.query.sort}`<\/code><\/td><td>Valider mod foruddefineret liste<\/td><\/tr><tr><td>4<\/td><td>Escaped input i stedet for parameterisering<\/td><td><code>mysql.escape(input)<\/code> i SQL-streng<\/td><td>Brug <code>?<\/code>-pladsholdere konsekvent<\/td><\/tr><tr><td>5<\/td><td>Databasefejl eksponeret til klient<\/td><td><code>res.json({ fejl: err.message })<\/code><\/td><td>Log internt, returner generisk besked<\/td><\/tr><tr><td>6<\/td><td>Prisma raw query med brugerinput<\/td><td><code>prisma.$queryRawUnsafe(sql)<\/code><\/td><td>Brug <code>prisma.$queryRaw`...`<\/code> syntax<\/td><\/tr><tr><td>7<\/td><td>Ingen validering af datatyper<\/td><td>Accepter alle strenge som ID<\/td><td>Valider at ID er positivt heltal med Zod<\/td><\/tr><tr><td>8<\/td><td>Multiple statements aktiveret<\/td><td><code>multipleStatements: true<\/code><\/td><td>Altid <code>multipleStatements: false<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerede-teknikker\">Avancerede teknikker<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"stored-procedures-som-ekstra-isolationslag\">Stored procedures som ekstra isolationslag<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Stored procedures isolerer SQL-logik i databasen og reducerer risikoen for, at applikationskoden introducerer injection-s\u00e5rbarheder. Applikationen kalder kun procedurenavnet med parametre og ser aldrig den underliggende SQL. Det er s\u00e6rligt nyttigt, n\u00e5r applikationsudviklerne og databaseadministratorerne er separate teams.<\/p>\n\n\n\n<pre><code class=\"language-sql\">-- Opret stored procedure i MySQL\nDELIMITER \/\/\nCREATE PROCEDURE HentBruger(IN p_id INT)\nBEGIN\n  SELECT id, navn, email, oprettet\n  FROM brugere\n  WHERE id = p_id;\nEND \/\/\nDELIMITER ;\n\n-- Giv applikationsbruger kun EXECUTE-rettighed (ikke direkte tabeladgang)\nGRANT EXECUTE ON PROCEDURE node_sikker_db.HentBruger TO 'app_user'@'localhost';\nREVOKE SELECT ON node_sikker_db.brugere FROM 'app_user'@'localhost';<\/code><\/pre>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ Kald stored procedure fra Node.js\nrouter.get('\/bruger\/:id', async (req, res) => {\n  const id = parseInt(req.params.id);\n  if (isNaN(id) || id < 1) {\n    return res.status(400).json({ fejl: 'Ugyldigt ID' });\n  }\n\n  const [rows] = await pool.execute('CALL HentBruger(?)', [id]);\n  const bruger = rows[0][0]; \/\/ Stored procedures returnerer nested array\n\n  if (!bruger) {\n    return res.status(404).json({ fejl: 'Bruger ikke fundet' });\n  }\n\n  res.json(bruger);\n});<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"runtime-beskyttelse-med-aikido-firewall\">Runtime-beskyttelse med Aikido Firewall<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e6rksniveau 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.<\/p>\n\n\n\n<pre><code class=\"language-bash\">npm install @aikidosec\/firewall<\/code><\/pre>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ KRITISK: Importer Aikido F\u00d8R alle andre moduler p\u00e5 f\u00f8rste linje\nrequire('@aikidosec\/firewall');\n\nconst express = require('express');\n\/\/ ... resten af din applikation<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"automatiseret-sikkerhedstjek-i-ci-cd\">Automatiseret sikkerhedstjek i CI\/CD<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">SQL injection introduceres ogs\u00e5 via kompromitterede npm-pakker. K\u00f8r automatiske sikkerhedstjek ved hvert commit med npm audit og Snyk:<\/p>\n\n\n\n<pre><code class=\"language-yaml\"># .github\/workflows\/sikkerhed.yml\nname: Sikkerhedstjek\n\non: [push, pull_request]\n\njobs:\n  sikkerhed:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: Installer Node.js 22\n        uses: actions\/setup-node@v4\n        with:\n          node-version: '22'\n\n      - name: Installer afh\u00e6ngigheder\n        run: npm ci\n\n      - name: npm audit - fejl ved h\u00f8j sv\u00e6rhedsgrad\n        run: npm audit --audit-level=high\n\n      - name: Snyk sikkerhedstjek\n        uses: snyk\/actions\/node@master\n        env:\n          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}\n        with:\n          args: --severity-threshold=high<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"komplet-arbejdende-projekt\">Komplet arbejdende projekt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Her er den fulde <code>src\/app.js<\/code> der samler alle teknikker fra guiden til en produktionsklar applikation:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">\/\/ src\/app.js - Komplet sikker Node.js API mod SQL injection\nrequire('@aikidosec\/firewall'); \/\/ Altid f\u00f8rste linje\nrequire('dotenv').config();\n\nconst express = require('express');\nconst helmet = require('helmet');\nconst sikkerBrugereRouter = require('.\/routes\/sikker-brugere');\nconst produkterRouter = require('.\/routes\/produkter');\nconst globalFejlh\u00e5ndterer = require('.\/middleware\/fejlh\u00e5ndtering');\n\nconst app = express();\n\n\/\/ Sikkerhedsheadere (CSP, HSTS, X-Frame-Options, osv.)\napp.use(helmet());\n\n\/\/ Body parsing med st\u00f8rrelsesbegr\u00e6nsning mod DoS\napp.use(express.json({ limit: '10kb' }));\napp.use(express.urlencoded({ extended: true, limit: '10kb' }));\n\n\/\/ Routes\napp.use('\/api\/brugere', sikkerBrugereRouter);\napp.use('\/api\/produkter', produkterRouter);\n\n\/\/ 404 h\u00e5ndtering\napp.use((req, res) => {\n  res.status(404).json({ fejl: 'Ressource ikke fundet' });\n});\n\n\/\/ Global fejlh\u00e5ndtering - altid sidst\napp.use(globalFejlh\u00e5ndterer);\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.log(`Sikker server k\u00f8rer p\u00e5 port ${PORT}`);\n});\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ops\u00e6t testdatabasen og k\u00f8r projektet:<\/p>\n\n\n\n<pre><code class=\"language-bash\"># S\u00e6t testdatabasetabeller op (MySQL)\nmysql -u root -p << 'ENDSQL'\nCREATE DATABASE IF NOT EXISTS node_sikker_db;\nUSE node_sikker_db;\nCREATE TABLE IF NOT EXISTS brugere (\n  id INT AUTO_INCREMENT PRIMARY KEY,\n  navn VARCHAR(100) NOT NULL,\n  email VARCHAR(255) NOT NULL UNIQUE,\n  alder INT,\n  oprettet DATETIME DEFAULT CURRENT_TIMESTAMP\n);\nCREATE TABLE IF NOT EXISTS produkter (\n  id INT AUTO_INCREMENT PRIMARY KEY,\n  navn VARCHAR(200) NOT NULL,\n  pris DECIMAL(10,2) NOT NULL,\n  kategori VARCHAR(50) NOT NULL,\n  oprettet DATETIME DEFAULT CURRENT_TIMESTAMP\n);\nINSERT INTO brugere (navn, email, alder) VALUES\n  ('Anders Hansen', 'anders@eksempel.dk', 32),\n  ('Marie Nielsen', 'marie@eksempel.dk', 28);\nENDSQL\n\n# Start applikationen\nnode src\/app.js\n\n# Test sikker s\u00f8gning\ncurl \"http:\/\/localhost:3000\/api\/brugere\/s\u00f8g?q=Anders\"\n# Output: [{\"id\":1,\"navn\":\"Anders Hansen\",\"email\":\"anders@eksempel.dk\"}]\n\n# Test valideringsfejl ved for kort s\u00f8geterm\ncurl \"http:\/\/localhost:3000\/api\/brugere\/s\u00f8g?q=A\"\n# Output: {\"fejl\":\"Valideringsfejl\",\"detaljer\":[{\"felt\":\"q\",\"besked\":\"S\u00f8gning skal v\u00e6re mindst 2 tegn\"}]}\n\n# Test SQL injection-fors\u00f8g - returnerer valideringsfejl, aldrig SQL-fejl\ncurl \"http:\/\/localhost:3000\/api\/brugere\/s\u00f8g?q=' OR '1'='1\"\n# Output: {\"fejl\":\"Valideringsfejl\",\"detaljer\":[{\"felt\":\"q\",\"besked\":\"Ugyldige tegn i s\u00f8gning\"}]}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fejlsoegningsguide-8-typiske-problemer\">Fejls\u00f8gningsguide: 8 typiske problemer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 1: mysql2 kaster \"Cannot read properties of undefined (reading 'execute')\"<\/strong><br>\u00c5rsag: Pool ikke oprettet korrekt, eller du importerer <code>mysql<\/code> i stedet for <code>mysql2<\/code>.<br>L\u00f8sning: Tjek at du importerer <code>mysql2\/promise<\/code> og at alle milj\u00f8variabler er sat. K\u00f8r <code>console.log(pool)<\/code> for at bekr\u00e6fte, at pool-objektet eksisterer og er et Pool-objekt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 2: Prisma kaster \"PrismaClientInitializationError\"<\/strong><br>\u00c5rsag: <code>DATABASE_URL<\/code> milj\u00f8variabel mangler, eller Prisma Client er ikke genereret efter schema-\u00e6ndring.<br>L\u00f8sning: K\u00f8r <code>npx prisma generate<\/code> efter enhver \u00e6ndring af <code>schema.prisma<\/code>. Bekr\u00e6ft at <code>.env<\/code> indeholder <code>DATABASE_URL=mysql:\/\/bruger:password@localhost:3306\/dbnavn<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 3: Zod afviser gyldige dansk input med specialtegn<\/strong><br>\u00c5rsag: Regex-m\u00f8nstret ekskluderer \u00e6, \u00f8, \u00e5 eller andre gyldige tegn.<br>L\u00f8sning: Test regex separat med <code>node -e \"console.log(\/^[a-zA-Z\u00e6\u00f8\u00e5\u00c6\u00d8\u00c5\\s\\-']+$\/.test('S\u00f8ren \u00d8degaard'))\"<\/code>. Tilf\u00f8j manglende tegn til m\u00f8nstret, herunder bindestreg og apostrof til navne.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 4: sqlmap rapporterer ingen injection trods s\u00e5rbar kode<\/strong><br>\u00c5rsag: sqlmap tester standardm\u00e6ssigt p\u00e5 niveau 1. Komplekse injection-punkter kr\u00e6ver h\u00f8jere niveau.<br>L\u00f8sning: K\u00f8r med <code>--level=5 --risk=3<\/code> for aggressiv testning. Supplement med manuelle tests: send <code>' OR '1'='1<\/code> direkte og tjek serverloggen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 5: pg returnerer tom array for foresp\u00f8rgsler der burde give resultater<\/strong><br>\u00c5rsag: PostgreSQL LIKE er case-sensitiv og matcher ikke store\/sm\u00e5 bogstaver.<br>L\u00f8sning: Brug <code>ILIKE<\/code> i stedet for <code>LIKE<\/code> til case-insensitiv s\u00f8gning i PostgreSQL: <code>WHERE navn ILIKE $1<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 6: Stored procedure kalder fejler med \"PROCEDURE does not exist\"<\/strong><br>\u00c5rsag: Applikationsbrugeren har ikke EXECUTE-rettighed til proceduren, eller proceduren er oprettet i en anden database.<br>L\u00f8sning: K\u00f8r <code>GRANT EXECUTE ON PROCEDURE node_sikker_db.HentBruger TO 'app_user'@'localhost'; FLUSH PRIVILEGES;<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 7: express-validator fejl vises ikke i respons selvom data er ugyldige<\/strong><br>\u00c5rsag: <code>tjekFejl<\/code>-middleware er ikke tilf\u00f8jet til route-k\u00e6den efter valideringsreglerne.<br>L\u00f8sning: Husk altid at inkludere <code>tjekFejl<\/code> mellem regler og handler: <code>router.post('\/sti', [...produktRegler], tjekFejl, handleProdukt)<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 8: Aikido Firewall blokerer legitime foresp\u00f8rgsler med SQL-lignende indhold<\/strong><br>\u00c5rsag: Firewall opdager et m\u00f8nster der ligner injection i lovlig data, f.eks. SQL-kodeeksempler i en blog-database.<br>L\u00f8sning: Konfigurer Aikido til at tillade specifikke endpoints via AIKIDO_BLOCK=false milj\u00f8variabel for det specifikke endpoint, eller brug Aikido-dashboardet til at justere regler.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"relateret-daekning\">Relateret d\u00e6kning<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"yderligere-node-js-sikkerhedsguides-paa-shattered-io\">Yderligere Node.js sikkerhedsguides p\u00e5 shattered.io<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/dk\/csrf-protection-nodejs\/\">CSRF Protection i Node.js: 12 trin [2026]<\/a><\/li>\n<li><a href=\"\/dk\/rate-limiting-nodejs\/\">Rate Limiting i Node.js: 12 trin p\u00e5 30 min [2026]<\/a><\/li>\n<li><a href=\"\/dk\/content-security-policy-nodejs\/\">Content Security Policy i Node.js: 12 trin [2026]<\/a><\/li>\n<li><a href=\"\/dk\/jwt-authentication-nodejs\/\">JWT Authentication i Node.js: 10 trin [2026]<\/a><\/li>\n<li><a href=\"\/dk\/nodejs-session-management\/\">Node.js Session Management: 11 trin [2026]<\/a><\/li>\n<li><a href=\"\/dk\/sikker-session-nodejs\/\">Sikker session i Node.js: 12 trin p\u00e5 30 min [2026]<\/a><\/li>\n<li><a href=\"\/dk\/hmac-webhook-signaturer-nodejs\/\">HMAC i Node.js: webhook-signaturer i 12 trin [2026]<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq-sql-injection-i-node-js\">FAQ: SQL Injection i Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"er-parameteriserede-forespoergsler-nok-til-at-stoppe-sql-injection\">Er parameteriserede foresp\u00f8rgsler nok til at stoppe SQL injection?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Parameteriserede foresp\u00f8rgsler er den prim\u00e6re og mest effektive beskyttelse mod SQL injection. De stopper angreb ved at adskille SQL-kode fra brugerdata p\u00e5 protokolniveau. Kombineret med input-validering og mindste privilegium giver de forsvar med 3 uafh\u00e6ngige lag, og alle tre skal v\u00e6re p\u00e5 plads i en produktionsapplikation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kan-jeg-bruge-string-escaping-i-stedet-for-parameterisering\">Kan jeg bruge string escaping i stedet for parameterisering?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nej. String escaping som <code>mysql.escape()<\/code> er fejlbeh\u00e6ftet og afh\u00e6nger af korrekt tegns\u00e6tkonfiguration. En forkert konfigureret databaseforbindelse kan omg\u00e5 escaping. Parameteriserede foresp\u00f8rgsler virker p\u00e5 protokolniveau og er ikke afh\u00e6ngige af tegns\u00e6t. Brug altid parameterisering.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"beskytter-prisma-orm-automatisk-mod-sql-injection\">Beskytter Prisma ORM automatisk mod SQL injection?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja, Prismas standard query-API parameteriserer automatisk alle v\u00e6rdier. Den eneste undtagelse er <code>prisma.$queryRawUnsafe()<\/code>, som aldrig m\u00e5 bruges med brugerinput. Brug i stedet <code>prisma.$queryRaw`...`<\/code> med tagged template literals, der parameteriserer sikkert.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvad-er-forskellen-paa-mysql2-query-og-execute\">Hvad er forskellen p\u00e5 mysql2 .query() og .execute()?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>pool.query()<\/code> sender foresp\u00f8rgslen og parametre i \u00e9n pakke. <code>pool.execute()<\/code> bruger prepared statements p\u00e5 MySQL-protokolniveau: SQL-teksten sendes og kompileres separat, derefter sendes parametre. Prepared statements er det st\u00e6rkeste forsvar og giver bedre ydeevne ved gentagne foresp\u00f8rgsler. Brug altid <code>execute()<\/code> med brugerinput.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"kan-nosql-databaser-som-mongodb-ogsaa-rammes-af-injection\">Kan NoSQL-databaser som MongoDB ogs\u00e5 rammes af injection?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Ja. MongoDB er s\u00e5rbar over for NoSQL injection, hvor angribere manipulerer query-objekter med operatorer som <code>$where<\/code> og <code>$gt<\/code>. Brug mongoose med schema-validering eller sanitiseringsbiblioteker som mongo-sanitize. Principperne om input-validering og mindste privilegium fra denne guide g\u00e6lder for alle databasetyper.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"skal-jeg-koere-sqlmap-i-produktion\">Skal jeg k\u00f8re sqlmap i produktion?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nej. K\u00f8r sqlmap kun mod dine egne udviklingsmilj\u00f8er eller dedikerede testmilj\u00f8er. sqlmap sender aggressive angrebspayloads der kan overbelaste databaser og generere store m\u00e6ngder falske log-alarmer. I produktion bruger du passive overv\u00e5gningsv\u00e6rkt\u00f8jer som Aikido Firewall og centraliseret log-analyse til at opdage angrebsfors\u00f8g.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"hvad-goer-jeg-hvis-jeg-opdager-sql-injection-i-eksisterende-produktionskode\">Hvad g\u00f8r jeg, hvis jeg opdager SQL injection i eksisterende produktionskode?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Behandl det som en sikkerhedsh\u00e6ndelse. Tjek omg\u00e5ende logs for tegn p\u00e5 udnyttelse. Implementer en midlertidig WAF-regel eller rate limit p\u00e5 det s\u00e5rbare endpoint. Ret koden: udskift streng-sammens\u00e6tning med parameteriserede foresp\u00f8rgsler. Deploy via din normale release-proces. K\u00f8r sqlmap mod det rettede endpoint for at verificere. Dokumenter h\u00e6ndelsen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"virker-disse-teknikker-med-typescript\">Virker disse teknikker med TypeScript?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e6lper med at forhindre SQL injection, fordi streng type-kontrol g\u00f8r det sv\u00e6rere at konvertere objekter til strenge i SQL-foresp\u00f8rgsler utilsigtet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Vil du styrke din Node.js-applikation yderligere? L\u00e6s <a href=\"https:\/\/owasp.org\/www-project-top-ten\/\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Top 10<\/a> for en oversigt over de mest kritiske webapplikationss\u00e5rbarheder, og <a href=\"https:\/\/nodejs.org\/learn\/getting-started\/security-best-practices\" target=\"_blank\" rel=\"noopener noreferrer\">Node.js officielle sikkerhedsguide<\/a> for platformsspecifikke anbefalinger. Snyk dokumenterer l\u00f8bende opdateringer om <a href=\"https:\/\/snyk.io\/blog\/preventing-sql-injection-attacks-node-js\/\" target=\"_blank\" rel=\"noopener noreferrer\">SQL injection-forsvar i Node.js<\/a>, og Prisma dokumenterer <a href=\"https:\/\/www.prisma.io\/docs\/orm\/prisma-client\/queries\/raw-database-access\/raw-queries\" target=\"_blank\" rel=\"noopener noreferrer\">sikker brug af raw queries<\/a> i detaljer. StackHawk udgav i 2025 en teknisk guide til <a href=\"https:\/\/www.stackhawk.com\/blog\/node-js-sql-injection-guide-examples-and-prevention\/\" target=\"_blank\" rel=\"noopener noreferrer\">Node.js SQL injection-eksempler og forebyggelse<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>SQL injection er stadig en af de mest udbredte s\u00e5rbarheder i webapplikationer i 2026. En enkelt usikker databaseforesp\u00f8rgsel giver angribere adgang til at l\u00e6se, \u00e6ndre eller slette hele din database\u2026<\/p>\n","protected":false},"author":6,"featured_media":122,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-121","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/121","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/comments?post=121"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/121\/revisions"}],"predecessor-version":[{"id":123,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/121\/revisions\/123"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media\/122"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media?parent=121"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/categories?post=121"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/tags?post=121"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}