SQL injection er den mest utbredte websikkerhetssvakheten i 2026 og rangerer som A03:2021 i OWASP Top 10. Et vellykket angrep gir en angriper full tilgang til databasen din, inkludert passord, betalingsdata og personlig informasjon for millioner av brukere. Node.js-applikasjoner er ikke immune, særlig når utviklere setter brukerinndataer direkte inn i SQL-spørringer uten validering eller parametrisering.

Denne opplæringen viser deg nøyaktig hvordan du sikrer en Express.js-applikasjon mot SQL injection i 12 konkrete steg. Du lærer å bruke parameteriserte spørringer med pg (node-postgres) og mysql2, beskytte deg gjennom ORMer som Sequelize, validere inndata med express-validator og Joi, og teste løsningen med bransjeverktøy. Alle eksempler er testet med Node.js 22 LTS.

Hva er SQL injection, og hvorfor er det farlig?

SQL injection skjer når en angriper setter ondsinnet SQL-kode inn i et inndatafelt som sendes ukontrollert videre til en database. Tenk deg et innloggingsskjema der applikasjonen bygger spørringen slik:

// SÅRBAR KODE - aldri gjør dette
const brukernavn = req.body.username;
const passord = req.body.password;
const sql = `SELECT * FROM brukere WHERE brukernavn = '${brukernavn}' AND passord = '${passord}'`;

Dersom en angriper skriver inn ' OR '1'='1 som brukernavn, blir spørringen:

SELECT * FROM brukere WHERE brukernavn = '' OR '1'='1' AND passord = 'hvasomhelst'

Betingelsen '1'='1' er alltid sann, og angriperen logger inn som den første brukeren i databasen, typisk en administrator, uten å kjenne passordet. I mer avanserte angrep kan hackeren lese alle tabeller, eksfiltrere passord-hashes, endre data, eller i verste fall kjøre systemkommandoer direkte på databaseserveren.

OWASP estimerer at injeksjonsangrep, inkludert SQL injection, rammer rundt 94 prosent av applikasjonene som testes for denne typen svakhet. Ifølge IBM Cost of a Data Breach-rapporten for 2024 koster et datainnbrudd i gjennomsnitt 4,88 millioner dollar, og SQL injection er en av de hyppigste inngangsvektorer.

Forutsetninger

For å følge denne opplæringen trenger du:

  • Node.js 22 LTS eller nyere (langsiktig støtte, anbefalt for produksjon)
  • npm versjon 10 eller nyere
  • PostgreSQL 16 eller MySQL 8.0 (ett av dem er tilstrekkelig)
  • Grunnleggende kunnskap om Express.js og asynkron JavaScript
  • En terminal med tilgang til databasen (lokal installasjon eller Docker)

Verifiser Node.js-versjon med:

node --version
# Forventet output: v22.x.x eller høyere

npm --version
# Forventet output: 10.x.x eller høyere

Steg 1: Prosjektoppsett og avhengigheter

Opprett et nytt Express.js-prosjekt og installer alle nødvendige pakker:

mkdir node-sql-sikkerhet && cd node-sql-sikkerhet
npm init -y

# Kjernepakker
npm install express pg mysql2

# ORM og spørringsbygger
npm install sequelize knex

# Valideringspakker
npm install express-validator joi

# Hjelpepakker
npm install dotenv morgan

# Utviklingsavhengigheter
npm install --save-dev nodemon

Opprett en grunnleggende prosjektstruktur:

node-sql-sikkerhet/
├── src/
│   ├── app.js
│   ├── db/
│   │   ├── pg-klient.js
│   │   └── mysql-klient.js
│   ├── ruter/
│   │   └── brukere.js
│   └── middleware/
│       └── validering.js
├── .env
└── package.json

Opprett .env-filen med tilkoblingsdetaljer. Legg aldri denne filen i versjonskontroll:

# .env
PG_HOST=localhost
PG_PORT=5432
PG_DATABASE=testdb
PG_USER=appbruker
PG_PASSWORD=sterkt_passord_her

MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=testdb
MYSQL_USER=appbruker
MYSQL_PASSWORD=sterkt_passord_her

PORT=3000

Steg 2: Identifiser sårbar kode i eksisterende prosjekter

Før du begynner å sikre ny kode, er det viktig å finne eksisterende SQL injection-sårbarheter. Her er de vanligste mønstrene å lete etter:

Sårbart mønsterEksempelRisiko
Strenginterpolasjon i SQL`SELECT * FROM x WHERE id=${req.params.id}`Kritisk
Strengkonkatenering"SELECT * FROM x WHERE id=" + idKritisk
Usikret ORDER BY`ORDER BY ${req.query.sort}`Høy
Dynamisk tabellnavn`SELECT * FROM ${req.body.tabell}`Kritisk
LIKE-spørring uten parameterisering`WHERE navn LIKE '%${søk}%'`Høy
IN-klausul med brukerdata`WHERE id IN (${req.body.ids})`Kritisk

Bruk grep for å finne potensielle sårbarheter i kodebasen din:

# Søk etter SQL-strenginterpolasjon
grep -rn "query\s*(\`" src/
grep -rn "query\s*(\".*\+" src/
grep -rn "execute\s*(\`" src/

# Se etter req.body/params/query direkte i SQL
grep -rn "req\.body\|req\.params\|req\.query" src/ | grep -i "select\|insert\|update\|delete"

Alle treff fra grep-søket over krever umiddelbar gjennomgang. I de neste stegene viser vi deg hvordan du erstatter hvert mønster med sikre alternativer.

Steg 3: Parameteriserte spørringer med PostgreSQL (pg)

Node-postgres (pg) er den mest brukte Node.js-driveren for PostgreSQL. Pakken støtter parameteriserte spørringer via $1, $2, $3-plassholdere. Parametrene sendes som en separat array, noe som gjør det umulig for databasen å tolke dem som SQL-kode.

Sett opp tilkoblingsbassenget i src/db/pg-klient.js:

// src/db/pg-klient.js
require('dotenv').config();
const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.PG_HOST,
  port: parseInt(process.env.PG_PORT, 10),
  database: process.env.PG_DATABASE,
  user: process.env.PG_USER,
  password: process.env.PG_PASSWORD,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

module.exports = pool;

Bruk alltid parameteriserte spørringer i ruterne dine:

// src/ruter/brukere.js - PostgreSQL-eksempler
const pool = require('../db/pg-klient');

// SIKKERT: Hent bruker basert på ID
async function hentBruker(req, res) {
  const { id } = req.params;

  // $1 er en plassholder - verdien sendes separat
  const resultat = await pool.query(
    'SELECT id, brukernavn, epost FROM brukere WHERE id = $1',
    [id]
  );

  if (resultat.rows.length === 0) {
    return res.status(404).json({ feil: 'Bruker ikke funnet' });
  }
  res.json(resultat.rows[0]);
}

// SIKKERT: Søk med LIKE (merk %-tegn utenfor parameteren)
async function søkBrukere(req, res) {
  const { q } = req.query;
  const søkemønster = `%${q}%`;

  const resultat = await pool.query(
    'SELECT id, brukernavn FROM brukere WHERE brukernavn ILIKE $1 LIMIT 20',
    [søkemønster]
  );

  res.json(resultat.rows);
}

// SIKKERT: Sett inn ny bruker
async function opprettBruker(req, res) {
  const { brukernavn, epost } = req.body;

  const resultat = await pool.query(
    'INSERT INTO brukere (brukernavn, epost, opprettet) VALUES ($1, $2, NOW()) RETURNING id',
    [brukernavn, epost]
  );

  res.status(201).json({ id: resultat.rows[0].id });
}

module.exports = { hentBruker, søkBrukere, opprettBruker };

Legg merke til at %-tegnet for LIKE-søket er plassert utenfor parameteren, altså i JavaScript-strengen søkemønster. Det er korrekt. Databasen behandler aldri q-variabelen som SQL-kode, bare som en ren streng.

Steg 4: Parameteriserte spørringer med MySQL og mysql2

Mysql2-pakken er den anbefalte MySQL-driveren for Node.js i 2026. Den støtter Promises, asynkron/avvent-mønster, og parameteriserte spørringer via ?-plassholdere. Viktig å merke seg: mysql2 slår av støtte for flere setninger i én spørring som standard, noe som blokkerer en vanlig angrepsteknikk.

// src/db/mysql-klient.js
require('dotenv').config();
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.MYSQL_HOST,
  port: parseInt(process.env.MYSQL_PORT, 10),
  database: process.env.MYSQL_DATABASE,
  user: process.env.MYSQL_USER,
  password: process.env.MYSQL_PASSWORD,
  waitForConnections: true,
  connectionLimit: 10,
  multipleStatements: false,  // Kritisk sikkerhetsinnstilling
  timezone: 'Z'
});

module.exports = pool;

Bruk execute() i stedet for query() der det er mulig. execute() sender spørringen til MySQL som en forberedt setning, noe som gir enda sterkere beskyttelse:

// src/ruter/brukere-mysql.js
const pool = require('../db/mysql-klient');

// SIKKERT: execute() bruker forberedte setninger på databasesiden
async function autentiserBruker(req, res) {
  const { brukernavn, passord } = req.body;

  // Parametrene sendes separat fra SQL-teksten
  const [rader] = await pool.execute(
    'SELECT id, brukernavn, passord_hash FROM brukere WHERE brukernavn = ?',
    [brukernavn]
  );

  if (rader.length === 0) {
    return res.status(401).json({ feil: 'Ugyldig brukernavn eller passord' });
  }

  // Sammenlign passord med hash (bruk bcrypt eller argon2)
  const bruker = rader[0];
  // const erGyldig = await bcrypt.compare(passord, bruker.passord_hash);

  res.json({ id: bruker.id, brukernavn: bruker.brukernavn });
}

// SIKKERT: Dynamisk ORDER BY med hviteliste
async function hentBrukere(req, res) {
  const tillatteKolonner = ['id', 'brukernavn', 'epost', 'opprettet'];
  const sorteringskolonne = tillatteKolonner.includes(req.query.sorter)
    ? req.query.sorter
    : 'id';

  // Kolonnenavnet settes inn direkte (hvitelistet), verdier parametriseres
  const [rader] = await pool.execute(
    `SELECT id, brukernavn, epost FROM brukere ORDER BY ${sorteringskolonne} LIMIT ?`,
    [20]
  );

  res.json(rader);
}

module.exports = { autentiserBruker, hentBrukere };

Hvitelisteprinsippet for kolonnenavn er avgjørende. Du kan ikke parametrisere kolonnenavn og tabellnavn, bare verdier. Når dynamiske identifikatorer er nødvendig, sjekk alltid mot en fastkodet liste med tillatte verdier.

Steg 5: Sikre spørringer med Sequelize ORM

Sequelize er det mest brukte ORM-et for Node.js og støtter PostgreSQL, MySQL, MariaDB, SQLite og Microsoft SQL Server. Alle spørringer som går gjennom Sequelizes innebygde metoder parametriseres automatisk, noe som eliminerer risikoen for SQL injection i vanlige CRUD-operasjoner.

// Sequelize-modell og sikre spørringer
const { Sequelize, DataTypes, Op } = require('sequelize');

const sequelize = new Sequelize(
  process.env.PG_DATABASE,
  process.env.PG_USER,
  process.env.PG_PASSWORD,
  {
    host: process.env.PG_HOST,
    dialect: 'postgres',
    logging: false
  }
);

const Bruker = sequelize.define('Bruker', {
  brukernavn: { type: DataTypes.STRING(50), allowNull: false },
  epost: { type: DataTypes.STRING(100), allowNull: false, unique: true }
});

// SIKKERT: Sequelize parametriserer automatisk
async function finnBruker(brukernavn) {
  return await Bruker.findOne({ where: { brukernavn } });
}

// SIKKERT: Kompleks spørring med Op-operatorer
async function søkBrukere(søkeTekst) {
  return await Bruker.findAll({
    where: {
      [Op.or]: [
        { brukernavn: { [Op.iLike]: `%${søkeTekst}%` } },
        { epost: { [Op.iLike]: `%${søkeTekst}%` } }
      ]
    },
    attributes: ['id', 'brukernavn', 'epost'],
    limit: 20
  });
}

Når du absolutt trenger rå SQL med Sequelize, bruk alltid replacements eller bind-parametere:

// SIKKERT: Rå SQL med Sequelize - bruk replacements
const resultat = await sequelize.query(
  'SELECT * FROM brukere WHERE avdeling_id = :avdelingId AND aktiv = :aktiv',
  {
    replacements: { avdelingId: req.params.id, aktiv: true },
    type: Sequelize.QueryTypes.SELECT
  }
);

// FEIL - aldri gjør dette med Sequelize heller
// const feil = await sequelize.query(`SELECT * FROM brukere WHERE id = ${req.params.id}`);

Steg 6: Inndatavalidering med express-validator

Parameteriserte spørringer er den viktigste forsvarslinjen mot SQL injection. Inndatavalidering er det neste laget: den sørger for at data har riktig format og innhold før de i det hele tatt når databaselaget. Express-validator er et populært mellomvarebibliotek som integrerer sømløst med Express.js.

// src/middleware/validering.js
const { body, param, query, validationResult } = require('express-validator');

// Valideringsregler for brukerregistrering
const validerRegistrering = [
  body('brukernavn')
    .trim()
    .isAlphanumeric('nb-NO', { ignore: '_-' })
    .isLength({ min: 3, max: 50 })
    .withMessage('Brukernavnet må være 3-50 tegn og kun inneholde bokstaver, tall, _ og -'),

  body('epost')
    .trim()
    .normalizeEmail()
    .isEmail()
    .withMessage('Ugyldig e-postadresse'),

  body('passord')
    .isLength({ min: 12, max: 128 })
    .withMessage('Passordet må være mellom 12 og 128 tegn'),

  // Middleware-funksjon som kontrollerer resultatet
  (req, res, next) => {
    const feil = validationResult(req);
    if (!feil.isEmpty()) {
      return res.status(400).json({
        feil: feil.array().map(f => ({ felt: f.path, melding: f.msg }))
      });
    }
    next();
  }
];

// Valideringsregler for URL-parametre
const validerBrukerId = [
  param('id')
    .isInt({ min: 1 })
    .withMessage('ID må være et positivt heltall'),

  (req, res, next) => {
    const feil = validationResult(req);
    if (!feil.isEmpty()) {
      return res.status(400).json({ feil: feil.array() });
    }
    next();
  }
];

module.exports = { validerRegistrering, validerBrukerId };

Koble validering til ruter i app.js:

// src/app.js
require('dotenv').config();
const express = require('express');
const morgan = require('morgan');
const { validerRegistrering, validerBrukerId } = require('./middleware/validering');
const { hentBruker, opprettBruker } = require('./ruter/brukere');

const app = express();

app.use(express.json({ limit: '10kb' }));
app.use(morgan('combined'));

// Ruter med validering
app.get('/brukere/:id', validerBrukerId, hentBruker);
app.post('/brukere', validerRegistrering, opprettBruker);

app.listen(process.env.PORT || 3000, () => {
  console.log(`Server kjører på port ${process.env.PORT || 3000}`);
});

Steg 7: Inndatavalidering med Joi

Joi er et skjemavalideringsbibliotek som er spesielt kraftig for komplekse datastrukturer. Det er populært i prosjekter der du vil definere valideringslogikk separat fra mellomvarelagene.

// src/middleware/joi-skjemaer.js
const Joi = require('joi');

const registreringsSkjema = Joi.object({
  brukernavn: Joi.string()
    .alphanum()
    .min(3)
    .max(50)
    .required()
    .messages({
      'string.alphanum': 'Brukernavnet kan kun inneholde bokstaver og tall',
      'string.min': 'Brukernavnet må ha minst {#limit} tegn',
      'any.required': 'Brukernavn er påkrevd'
    }),

  epost: Joi.string()
    .email({ tlds: { allow: false } })
    .max(100)
    .required(),

  passord: Joi.string()
    .min(12)
    .max(128)
    .required(),

  alder: Joi.number()
    .integer()
    .min(13)
    .max(120)
    .optional()
});

// Gjenbrukbar Joi-mellomvare
function validerMedJoi(skjema) {
  return (req, res, next) => {
    const { error, value } = skjema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true
    });

    if (error) {
      return res.status(400).json({
        feil: error.details.map(d => ({
          felt: d.path.join('.'),
          melding: d.message
        }))
      });
    }

    // Erstatt req.body med validerte og normaliserte verdier
    req.body = value;
    next();
  };
}

module.exports = { registreringsSkjema, validerMedJoi };

Legg merke til stripUnknown: true. Denne innstillingen fjerner alle felt som ikke er definert i skjemaet, noe som forhindrer at uvedkommende felt sendes videre til databaselaget. Det er en enkel men effektiv tilleggssikring.

Steg 8: Databaserettigheter og minste privilegium

Selv om du bruker perfekte parameteriserte spørringer, kan en feil i applikasjonen potensielt gi angripere tilgang til databasen. Prinsippet om minste privilegium begrenser skaden: applikasjonsbrukeren skal bare ha de rettighetene den faktisk trenger.

Opprett en begrenset databasebruker for applikasjonen din i PostgreSQL:

-- PostgreSQL: Opprett begrenset applikasjonsbruker
CREATE USER appbruker WITH PASSWORD 'sterkt_passord_her';

-- Gi kun nødvendige rettigheter til applikasjonens tabeller
GRANT CONNECT ON DATABASE testdb TO appbruker;
GRANT USAGE ON SCHEMA public TO appbruker;

-- Kun SELECT, INSERT, UPDATE på de tabellene applikasjonen trenger
GRANT SELECT, INSERT, UPDATE ON TABLE brukere TO appbruker;
GRANT SELECT ON TABLE roller TO appbruker;
GRANT SELECT ON TABLE produkter TO appbruker;

-- ALDRI gi DELETE til applikasjonsbrukeren med mindre nødvendig
-- ALDRI gi DROP, CREATE, ALTER til applikasjonsbrukeren
-- ALDRI bruk superbruker-tilkobling i applikasjonen

-- For PostgreSQL-sekvenser (for automatisk ID-generering)
GRANT USAGE, SELECT ON SEQUENCE brukere_id_seq TO appbruker;

Tilsvarende for MySQL:

-- MySQL: Opprett begrenset applikasjonsbruker
CREATE USER 'appbruker'@'localhost' IDENTIFIED BY 'sterkt_passord_her';

-- Gi begrensede rettigheter
GRANT SELECT, INSERT, UPDATE ON testdb.brukere TO 'appbruker'@'localhost';
GRANT SELECT ON testdb.roller TO 'appbruker'@'localhost';
GRANT SELECT ON testdb.produkter TO 'appbruker'@'localhost';

FLUSH PRIVILEGES;
DatabaserettighetApplikasjonsbrukerMigrasjonsbrukerAdministrasjonsbruker
SELECTJa (spesifikke tabeller)JaJa
INSERTJa (spesifikke tabeller)JaJa
UPDATEJa (spesifikke tabeller)JaJa
DELETENei (unntaksvis)JaJa
CREATE / DROPNeiJaJa
TRUNCATENeiNeiJa
GRANT OPTIONNeiNeiJa

Steg 9: Lagrede prosedyrer og databasevisninger

Lagrede prosedyrer er forhåndskompilert SQL som kjøres på databaseserveren. Siden SQL-logikken er definert på databasesiden og ikke konstrueres dynamisk i Node.js, er lagrede prosedyrer i seg selv resistente mot SQL injection, forutsatt at de er riktig implementert.

-- PostgreSQL: Lagret prosedyre for sikker brukerautentisering
CREATE OR REPLACE FUNCTION autentiser_bruker(
  p_brukernavn VARCHAR(50),
  p_epost VARCHAR(100)
)
RETURNS TABLE(id INT, brukernavn VARCHAR, epost VARCHAR)
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
  RETURN QUERY
    SELECT b.id, b.brukernavn, b.epost
    FROM brukere b
    WHERE b.brukernavn = p_brukernavn
      AND b.aktiv = TRUE;
END;
$$;

Kall prosedyren fra Node.js med parametrisering:

// Kall lagret prosedyre fra Node.js (PostgreSQL)
async function autentiserMedProsedyre(brukernavn, epost) {
  const resultat = await pool.query(
    'SELECT * FROM autentiser_bruker($1, $2)',
    [brukernavn, epost]
  );
  return resultat.rows;
}

Databasevisninger (views) gir et ekstra sikkerhetslag ved å begrense hvilke kolonner applikasjonsbrukeren kan se. En applikasjonsbruker kan ha SELECT-rettighet på visningen, men ikke på selve tabellen, noe som skjuler sensitive kolonner som passord-hashes og interne felt.

Steg 10: Overvåking og logging av SQL-angrep

Selv den beste sikkerheten er verdifull kun om du oppdager angrepsforsøk. Konfigurer logging for å fange opp mistenkelige mønstre. For Node.js-applikasjoner er strukturert logging med et bibliotek som winston et godt valg.

// src/middleware/sql-logging.js
const SQL_INJEKSJON_MØNSTRE = [
  /('\s*(or|and)\s*'[^']*'?\s*=\s*')/gi,
  /(union\s+select)/gi,
  /(drop\s+table)/gi,
  /(insert\s+into.*values)/gi,
  /(--\s*$)/gm,
  /(\/\*[\s\S]*?\*\/)/g,
  /(\bexec\b|\bexecute\b)/gi,
  /(xp_cmdshell)/gi
];

function loggMistenkeligInndata(req, res, next) {
  const kontrollerStreng = (verdi, felt) => {
    if (typeof verdi !== 'string') return;
    for (const mønster of SQL_INJEKSJON_MØNSTRE) {
      if (mønster.test(verdi)) {
        console.warn(JSON.stringify({
          nivå: 'ADVARSEL',
          hendelse: 'mulig_sql_injeksjon',
          felt,
          ip: req.ip,
          metode: req.method,
          sti: req.path,
          tidspunkt: new Date().toISOString()
        }));
        break;
      }
    }
  };

  // Sjekk alle inndatakilder
  Object.entries(req.body || {}).forEach(([k, v]) => kontrollerStreng(v, `body.${k}`));
  Object.entries(req.params || {}).forEach(([k, v]) => kontrollerStreng(v, `params.${k}`));
  Object.entries(req.query || {}).forEach(([k, v]) => kontrollerStreng(v, `query.${k}`));

  next();
}

module.exports = { loggMistenkeligInndata };

Registrer mellomvaren i app.js for global logging:

const { loggMistenkeligInndata } = require('./middleware/sql-logging');

// Legg til globalt for alle ruter
app.use(loggMistenkeligInndata);

Steg 11: Test sikkerheten din mot SQL injection

Etter implementeringen må du verifisere at sikringstiltakene faktisk fungerer. Manuell testing kombinert med automatiserte verktøy gir den beste dekningen.

Manuell testing med curl:

# Test klassisk SQL injection-nyttelast i URL-parameter
curl -s "http://localhost:3000/brukere/1%20OR%201=1"
# Forventet svar: 400 Bad Request (validering blokkerer)

# Test i JSON-body
curl -s -X POST http://localhost:3000/brukere \
  -H "Content-Type: application/json" \
  -d '{"brukernavn": "admin'\''--", "epost": "[email protected]", "passord": "testpassord123"}'
# Forventet svar: 400 Bad Request (validering blokkerer apostrof)

# Test UNION-injeksjon
curl -s "http://localhost:3000/brukere/1%20UNION%20SELECT%201,2,3--"
# Forventet svar: 400 Bad Request

# Sjekk at normal forespørsel fungerer
curl -s "http://localhost:3000/brukere/1"
# Forventet svar: 200 OK med brukerdata

For mer grundig testing, bruk OWASP ZAP (Zed Attack Proxy). ZAP er et gratis og åpen kildekode-verktøy som automatisk scanner webapplikasjoner for kjente sårbarheter, inkludert SQL injection:

# Kjør OWASP ZAP med Docker
docker pull ghcr.io/zaproxy/zaproxy:stable

# Aktiv skanning av lokalt API
docker run --rm -t ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
  -t http://host.docker.internal:3000/api-definisjon.json \
  -f openapi \
  -r zap-rapport.html

# Enkel spider-skanning
docker run --rm -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t http://host.docker.internal:3000

Steg 12: Komplett prosjekt med alle sikringstiltak

Her er en komplett Express.js-applikasjon som kombinerer alle stegene ovenfor i et reelt eksempel. Koden demonstrerer dybdeforsvar: parameteriserte spørringer, validering, logging og prinsippet om minste privilegium i ett samlet system.

// src/app-komplett.js - Komplett sikker Node.js SQL-applikasjon
require('dotenv').config();
const express = require('express');
const { Pool } = require('pg');
const { body, param, validationResult } = require('express-validator');
const { loggMistenkeligInndata } = require('./middleware/sql-logging');

const app = express();
const pool = new Pool({
  host: process.env.PG_HOST,
  database: process.env.PG_DATABASE,
  user: process.env.PG_USER,
  password: process.env.PG_PASSWORD,
  max: 10
});

// Global mellomvare
app.use(express.json({ limit: '10kb' }));
app.use(loggMistenkeligInndata);

// Hjelpefunksjon: Standardiser feilsvar
const sendFeil = (res, status, melding) =>
  res.status(status).json({ feil: melding });

// Hjelpefunksjon: Kjør validering og returner feil
const valider = (req, res) => {
  const feil = validationResult(req);
  if (!feil.isEmpty()) {
    res.status(400).json({ feil: feil.array() });
    return false;
  }
  return true;
};

// GET /brukere/:id
app.get('/brukere/:id',
  param('id').isInt({ min: 1 }),
  async (req, res) => {
    if (!valider(req, res)) return;

    const { rows } = await pool.query(
      'SELECT id, brukernavn, epost FROM brukere WHERE id = $1',
      [req.params.id]
    );
    if (!rows.length) return sendFeil(res, 404, 'Bruker ikke funnet');
    res.json(rows[0]);
  }
);

// POST /brukere
app.post('/brukere',
  body('brukernavn').trim().isAlphanumeric().isLength({ min: 3, max: 50 }),
  body('epost').trim().normalizeEmail().isEmail(),
  body('passord').isLength({ min: 12, max: 128 }),
  async (req, res) => {
    if (!valider(req, res)) return;

    const { brukernavn, epost } = req.body;
    const { rows } = await pool.query(
      'INSERT INTO brukere (brukernavn, epost) VALUES ($1, $2) RETURNING id',
      [brukernavn, epost]
    );
    res.status(201).json({ id: rows[0].id });
  }
);

app.listen(3000, () => console.log('Sikker server kjører på port 3000'));

Kjør applikasjonen og kontroller at den starter uten feil:

node src/app-komplett.js
# Forventet output: Sikker server kjører på port 3000

# Test med gyldig forespørsel
curl -X POST http://localhost:3000/brukere \
  -H "Content-Type: application/json" \
  -d '{"brukernavn": "olajohnsen", "epost": "[email protected]", "passord": "SikkertPassord123!"}'
# Forventet svar: {"id": 1}

# Test med SQL injection-nyttelast
curl -X POST http://localhost:3000/brukere \
  -H "Content-Type: application/json" \
  -d '{"brukernavn": "admin'\''--", "epost": "[email protected]", "passord": "test"}'
# Forventet svar: {"feil": [{"type":"field","msg":"Invalid value","path":"brukernavn",...}]}

Vanlige fallgruver ved SQL injection-sikring

Disse feilene gjør seg gjeldende i produksjonskode selv hos erfarne utviklere:

Fallgruve 1: Bruk av strenginterpolasjon for kolonnenavn og tabellnavn. Du kan ikke parametrisere identifikatorer (kolonner, tabeller, skjemaer) i SQL, bare verdier. Mange utviklere oppdager dette og tyr til strenginterpolasjon uten hvitelisting. Løsningen er alltid å sjekke mot en fastkodet liste med tillatte verdier, som vist i mysql2-eksempelet over.

Fallgruve 2: Kun sanitering uten parametrisering. Noen forsøker å fjerne farlige tegn (apostrof, semikolon osv.) med regulære uttrykk. Dette er ikke tilstrekkelig. Det finnes mange kodeinger og omgåelser som unngår enkel sanitering. Parameteriserte spørringer er den eneste pålitelige beskyttelsen.

Fallgruve 3: Glemme å validere numeriske parametre. Mange antar at URL-parametre som :id er trygge fordi de ser ut som tall. Men uten validering kan /brukere/1%20OR%201=1 sendes som ID. Bruk alltid isInt() eller tilsvarende validering for numeriske felt.

Fallgruve 4: Feilhåndtering som avslører SQL-struktur. Når databaseen kaster en feil, sender mange utviklere feilmeldingen direkte til klienten. En SQL-feilmelding kan avsløre tabellnavn, kolonnenavn og databaseversjoner, informasjon angripere bruker til videre angrep. Logg alltid detaljer internt og send generiske feilmeldinger til klienten.

// FEIL - avslører databasestruktur
app.get('/bruker/:id', async (req, res) => {
  const resultat = await pool.query(...).catch(err => {
    res.status(500).json({ feil: err.message }); // Aldri gjør dette
  });
});

// RIKTIG - generisk feilmelding til klienten
app.get('/bruker/:id', async (req, res) => {
  try {
    const resultat = await pool.query('SELECT * FROM brukere WHERE id = $1', [req.params.id]);
    res.json(resultat.rows[0]);
  } catch (err) {
    console.error('Databasefeil:', err); // Logg internt
    res.status(500).json({ feil: 'En intern feil oppstod' }); // Generisk til klient
  }
});

Fallgruve 5: Stole på klientside-validering alene. JavaScript-validering i nettleseren kan omgås med enkle verktøy som curl eller Burp Suite. Serverside-validering i Node.js er ikke valgfri; den er obligatorisk.

Fallgruve 6: Ikke oppdatere databasedrivere. Sikkerhetsproblemer oppdages jevnlig i populære npm-pakker. Kjør npm audit jevnlig og oppdater pg, mysql2 og Sequelize til nyeste stabile versjon.

Fallgruve 7: Bruke for brede databaserettigheter i utvikling. Utviklere bruker ofte superbrukertilkoblinger lokalt for enkelhets skyld, og glemmer å bytte til begrenset bruker før produksjonssetting. Definer begrensede brukere fra dag én, og bruk dem i alle miljøer.

Feilsøking: Vanlige problemer og løsninger

Problem 1: “pg” kaster “invalid input syntax for type integer” ved parameterisering.

Dette skjer når Node.js sender en streng der PostgreSQL forventer et heltall. Konverter alltid parametre til riktig type med parseInt(verdi, 10) eller la express-validator håndtere typekonvertering med .toInt().

Problem 2: mysql2 kaster “ER_PARSE_ERROR” på ellers korrekte spørringer.

Sjekk at du bruker riktig antall ?-plassholdere og at verdiarrayen har like mange elementer. Et vanlig problem er å glemme en parameter eller sende undefined i arrayen. Legg til konsolllogging av spørringen og parametrene for debugging.

Problem 3: Sequelize genererer uventede SQL-spørringer i produksjon.

Aktiver Sequelize-logging midlertidig for å se eksakt SQL som genereres: new Sequelize(..., { logging: console.log }). Aldri la detaljert logging stå aktivert i produksjon da det avslører tabellenavn og datastrukturer i applikasjonslogger.

Problem 4: express-validator blokkerer gyldige forespørsler.

Sjekk lokalinnstillingene for isAlphanumeric(). Norske bokstaver som æ, ø og å anses ikke som alfanumeriske i standardinnstillingene. Bruk isAlphanumeric('nb-NO') for norsk, eller bruk matches(/^[a-zA-ZæøåÆØÅ0-9_-]+$/) for tilpasset validering.

Problem 5: Tilkoblingspool er oppbrukt under høy last.

Sørg for at alle databasespørringer er inne i try/catch og at tilkoblinger alltid frigis. Med pg og eksplisitt klientsjekk ut, husk klient.release() i finally-blokken. Bruk helst pool.query() direkte og la biblioteket håndtere tilkoblingene.

Problem 6: UNION SELECT-angrep omgår validering.

Hvis validering stoppes av et UNION SELECT-forsøk men ikke fanger det, sjekk at valideringsreglene dekker alle inndatakilder (req.body, req.params, req.query, req.headers). Headers som X-Custom-Header er en vanlig oversett injeksjonsvektor.

Problem 7: npm audit rapporterer kritiske sårbarheter i pg eller mysql2.

Kjør npm audit fix for automatiske oppdateringer. For alvorlige sårbarheter, sjekk om applikasjonen din faktisk bruker den sårbare kodestien. Bruk npm audit --json for å eksportere rapporten og analyser nøye før produksjonsoppdatering.

Problem 8: Databaseloggen viser mange forbindelsesfeil etter sikringsimplementering.

Sjekk at .env-filen er riktig konfigurert og at miljøvariablene lastes. Kjør console.log(process.env.PG_USER) som debuggingkontroll. Verifiser at applikasjonsbrukeren har de rettigheter du forventer ved å kjøre en testspørring direkte i psql eller MySQL Workbench med applikasjonsbrukerens legitimasjon.

Avanserte tips for produksjonsklare applikasjoner

Bruk prepared statements på databasenivå for kritiske spørringer. Med PostgreSQL kan du opprette navngitte prepared statements som sendes og kompileres én gang, deretter brukes gjentatte ganger. Dette gir både ytelsesgevinst og maksimal injeksjonsbeskyttelse:

// Forberedt setning med pg - opprettes én gang ved oppstart
await pool.query({
  name: 'hent-bruker-etter-id',
  text: 'SELECT id, brukernavn, epost FROM brukere WHERE id = $1',
  values: [1]
});

// Gjenbruk - databasen bruker hurtigbuffret kjøringsplan
const resultat = await pool.query({
  name: 'hent-bruker-etter-id',
  values: [req.params.id]
});

Implementer rate limiting spesifikt for databasetunge endepunkter. SQL injection-angrep er ofte automatiserte og sender hundrevis av forespørsler per sekund for å kartlegge databasestrukturen. Kombiner generell rate limiting med strengere grenser for søk og innloggingspunkter. Les vår guide om Helmet.js i Node.js for ytterligere sikkerhetsoverskrifter som beskytter Express-applikasjoner.

Aktiver parameterisert logging i databasen. PostgreSQL sin pg_stat_statements-utvidelse registrerer alle SQL-spørringer med $1-plassholdere bevart, noe som gjør det mulig å analysere spørringsmønstre uten å eksponere faktiske verdier. Dette er uvurderlig for å oppdage uvanlige spørringsmønstre som kan indikere et pågående angrep.

Integrer sikkerhetsscanning i CI/CD-rørledningen. Legg til npm audit og Snyk-skanning som obligatoriske trinn i CI-pipelinen. En sårbar avhengighet som glir gjennom til produksjon kan undergrave selv den beste parameteriseringen. Sett opp automatisk varsel når kritiske sårbarheter oppdages i npm-registeret for pakker du bruker.

Sammenligning av Node.js-databibliotekers SQL injection-beskyttelse

BibliotekParametriseringAutomatiskRå SQL-støtteAnbefaling
pg (node-postgres)$1, $2, $3ManuellJa, med plassholdereExcellent
mysql2?ManuellJa, execute() foretrukketExcellent
Sequelize ORMAutomatisk via metoderAutomatiskJa, med replacementsExcellent
Knex.jsAutomatisk via builderAutomatiskJa, med bindingsMeget god
Prisma ORMAutomatisk alltidAutomatiskBegrenset, tagget templatesExcellent
better-sqlite3?-plassholdereManuellJa, med parametreGod (kun SQLite)

Relatert innhold

Bygg videre på kunnskapen din om Node.js-sikkerhet med disse artiklene fra shattered.io:

Ofte stilte spørsmål

Er parameteriserte spørringer nok til å forhindre alle SQL injection-angrep?

Parameteriserte spørringer beskytter mot nesten alle varianter av SQL injection når de brukes konsekvent. De beskytter imidlertid ikke mot logiske feil i SQL-strukturen, feil rettighetsoppsett, eller second-order injection der data lagres trygt men brukes usikkert i en annen spørring senere. Dybdeforsvar, det vil si lag på lag med sikringstiltak, er alltid den riktige tilnærmingen.

Bør jeg bruke et ORM fremfor direktedrivere for å unngå SQL injection?

Et ORM som Sequelize gir automatisk parametrisering og reduserer risikoen for menneskelige feil. Direktedrivere som pg og mysql2 er like sikre når du bruker dem riktig, men krever mer disiplin. Velg basert på prosjektets kompleksitet: direktedrivere for enkel applikasjoner med høy ytelseskrav, ORM for komplekse domenemodeller der CRUD-operasjoner dominerer.

Hva er second-order SQL injection og hvordan beskyttes man mot det?

Second-order injection skjer når ondsinnet data lagres trygt i databasen (korrekt parametrisert), men siden brukes i en ny spørring uten parametrisering. For eksempel: et brukernavn med apostrof lagres trygt, men brukes siden i en ny spørring som konkatenerer strengen. Beskyttelsen er å alltid parametrisere alle spørringer, uansett om dataene kommer fra brukeren direkte eller fra databasen.

Kan jeg bruke en WAF (Web Application Firewall) i stedet for å fikse koden?

En WAF er et nyttig ekstra lag, men den er ikke en erstatning for kodefikser. WAFer kan omgås med avanserte nyttelaster og tilsløring. Den eneste pålitelige løsningen er kodeendringer: parameteriserte spørringer, inndatavalidering og prinsippet om minste privilegium i kodebasen din.

Hvor ofte bør jeg kjøre npm audit?

Kjør npm audit ved hver deploy og minst en gang per uke i aktive prosjekter. Integrer det i CI/CD-pipelinen din slik at nye kritiske sårbarheter blokkerer produksjonsdeploy. Tjenester som Snyk og GitHub Dependabot kan sende varsler automatisk når nye sårbarheter oppdages i avhengighetene dine.

Gjelder SQL injection-risiko også for NoSQL-databaser som MongoDB?

MongoDB og andre NoSQL-databaser er immune mot tradisjonell SQL injection, men sårbare for NoSQL injection, der angriperen setter inn JavaScript-objekter med operatorer som $where eller $gt direkte i spørringer. Prinsippene er de samme: valider og sanitize alle inndataer, og bruk ODM-biblioteker som Mongoose som håndterer typekontroll og desinfisering automatisk.

Er det sikkert å vise SQL-feilmeldinger i utviklingsmiljøet?

I et isolert lokalt utviklingsmiljø er detaljerte feilmeldinger nyttige. Bruk miljøvariabelen NODE_ENV til å kontrollere feilnivået: detaljert i development, generisk i production. Aldri send databasefeilmeldinger til klienten i produksjon, og aldri logg faktiske SQL-spørringer med parameterverdier i applikasjonsloggene da de kan inneholde sensitiv brukerinformasjon.