Helmet.js setter 11 sikkerhetshoder i Express på én kodelinje. Uten dem sender applikasjonen din informasjon om server-teknologi, tillater iframe-inlasting fra tredjeparter og mangler beskyttelse mot MIME-sniffing-angrep. Pakken lastes ned over 2 millioner ganger per uke og er anbefalt direkte av Node.js-sikkerhetsteamet. Denne guiden tar deg gjennom et komplett oppsett i 12 steg med Helmet.js 8.2.0.

Hva er Helmet.js og hvorfor trenger du det?

Helmet.js er en Express-mellomvare som setter HTTP-sikkerhetshoder automatisk. Express leverer ingen sikkerhetshoder som standard, noe som betyr at svar fra en ny Express-applikasjon avslører server-teknologi via X-Powered-By: Express og mangler alle de header-baserte forsvarslinjene som moderne nettlesere forventer.

HTTP-sikkerhetshoder er instruksjoner fra serveren til nettleseren om hvordan siden skal håndteres sikkerhetsmessig. De definerer hvilke ressurser nettleseren har lov til å laste, om siden kan vises i en iframe, om HTTPS skal kreves for fremtidige besøk, og mye mer. Disse hodene er ikke opsjonale i 2026. OWASP lister manglende sikkerhetshoder som en av de mest utbredte webapplikasjonssårbarhetene, og EU-direktivet NIS2 krever av alle norske virksomheter over en viss størrelse at de implementerer tilstrekkelige tekniske sikkerhetstiltak.

En angriper som skanner etter sårbare Express-apper bruker akkurat manglende hoder som et signal. Manglende X-Frame-Options åpner for clickjacking. Manglende Content-Security-Policy betyr at skadelige skript kan injiseres via XSS. Manglende X-Content-Type-Options lar nettleseren gjette MIME-typen og kjøre eksekverbart innhold den ikke skulle kjørt. Tilstedeværelsen av X-Powered-By: Express bekrefter teknologi-stakken for automatiserte skanningsprogrammer som søker etter kjente Express-sårbarheter.

Helmet løser alle disse problemene på én gang. Pakken grupperer 13 individuelle sikkerhetsmoduler. Hvert modul setter ett eller flere HTTP-hoder. Du aktiverer alle med app.use(helmet()) eller konfigurerer dem enkeltvis etter behov. Helmet har over 9.400 stjerner på GitHub og er med i de offisielle Express-sikkerhetsretningslinjene.

Hvem er dette for: Alle som bygger REST-APIer eller webapplikasjoner med Node.js og Express. Enten du lager en hobbyapp eller en produksjonstjeneste med norske bedriftsdata og brukerkontoer, gjelder de samme kravene. Guiden forutsetter at du kjenner til grunnleggende Express-routing og npm.

HTTP-sikkerhetshoder forklart

Før vi setter opp Helmet, er det nyttig å forstå hva HTTP-sikkerhetshoder faktisk gjør og hvorfor nettlesere respekterer dem.

Når nettleseren din besøker en nettside, sender serveren tilbake et HTTP-svar. Dette svaret inneholder to deler: hoder og kropp. Kroppen er innholdet (HTML, JSON, osv.). Hodene er metadata som sier noe om innholdet og hvordan det skal behandles. Sikkerhetshoder er hoder som spesifikt instruerer nettleseren om sikkerhetsrelatert oppførsel.

Nettlesere fra alle store leverandører (Chrome, Firefox, Safari, Edge) har implementert støtte for disse hodene som del av webstandarden. Serveren trenger ikke gjøre noe spesielt utover å sende riktige hoder. Nettleseren leser dem og håndhever dem automatisk for brukeren.

Problemet er at dette er opt-in: serveren MÅ aktivt sende hodene. En server som ikke sender X-Frame-Options gir nettleseren ingen instruksjon om iframe-håndtering, og nettleseren velger da å tillate framing fra alle domener. Helmet løser dette ved å sette alle viktige hoder som standard, slik at du aktivt MÅ velge bort dem, ikke aktivt velge dem til.

Tabellen nedenfor viser angrepskategorier som header-mangler åpner for, og hvilken del av OWASP Top 10 de faller under:

AngrepskategoriOWASP Top 10 kategoriManglende hodeKonsekvens
Cross-Site Scripting (XSS)A03: InjectionContent-Security-PolicyInjeksjon og kjøring av skadelige skript
ClickjackingA05: Security MisconfigurationX-Frame-OptionsBrukere lures til å klikke på skjulte elementer
MIME-sniffingA05: Security MisconfigurationX-Content-Type-OptionsNettleser kjører filer med feil MIME-type
SSL-strippingA02: Cryptographic FailuresStrict-Transport-SecurityNedgradering fra HTTPS til HTTP
InformasjonslekkasjeA05: Security MisconfigurationX-Powered-By (fjernet)Teknologi-stack eksponert for angripere
Referrer-lekkasjeA02: Cryptographic FailuresReferrer-PolicySensitive URL-parametere lekker til tredjeparter

Forutsetninger

Før du starter, kontroller at følgende er på plass:

  • Node.js 22.x eller 24.x (LTS-versjoner støttet per juni 2026). Sjekk med node --version.
  • npm 10.x eller nyere. Sjekk med npm --version.
  • Express.js 4.x eller 5.x. Helmet 8.2.0 støtter begge versjonene.
  • Grunnleggende kunnskap om JavaScript og asynkron programmering i Node.js.
  • Et terminalvindu med tilgang til prosjektmappen din.
  • En HTTP-klient for testing, for eksempel curl eller Postman.
KomponentMinimumsversjonAnbefalt versjonFormål
Node.js18.x22.x LTSKjøretidsmiljø
Express4.185.xWeb-rammeverk
Helmet7.x8.2.0Sikkerhetshoder
npm8.x10.xPakkebehandler
curl7.x8.xTesting av hoder

Steg 1: Opprett prosjektstruktur

Start med et rent prosjekt. Dette gir deg full kontroll over avhengighetene og hjelper deg å forstå hva Helmet faktisk legger til.

mkdir helmet-demo && cd helmet-demo
npm init -y
mkdir src

Opprett filen src/app.js som utgangspunkt. Du bygger den ut steg for steg gjennom guiden. Guiden bruker CommonJS (require) for kompatibilitet med både Express 4.x og 5.x. Vil du bruke ES-moduler, legger du til "type": "module" i package.json og bytter require() til import-syntaks.

Prosjektstrukturen ser slik ut etter dette steget:

helmet-demo/
├── src/
│   └── app.js
├── package.json
└── node_modules/

Steg 2: Installer Helmet.js 8.2.0 og Express

Installer Helmet og Express med npm. nodemon er et utviklingsverktøy som automatisk starter serveren på nytt ved kodeendringer:

npm install express helmet
npm install --save-dev nodemon

Legg til startskript i package.json:

{
  "name": "helmet-demo",
  "version": "1.0.0",
  "description": "Helmet.js sikkerhetshoder demo",
  "main": "src/app.js",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js"
  },
  "dependencies": {
    "express": "^5.0.0",
    "helmet": "^8.2.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}

Bekreft at Helmet 8.2.0 er installert:

npm list helmet
# Forventet output:
# [email protected]
# └── [email protected]

Sjekk at ingen kjente sårbarheter finnes i avhengighetene dine allerede fra start:

npm audit
# Ideelt svar:
# found 0 vulnerabilities

Steg 3: Grunnleggende Helmet-oppsett

Legg til Helmet som mellomvare tidlig i mellomvare-stabelen din. Rekkefølge er avgjørende i Express: Helmet må stå før ruter og annen mellomvare som sender svar, fordi hoder ikke kan legges til etter at svaret er sendt.

Opprett src/app.js med følgende innhold:

const express = require('express');
const helmet = require('helmet');

const app = express();

// Helmet MÅ stå øverst i mellomvare-stabelen
app.use(helmet());

app.get('/', (req, res) => {
  res.json({ melding: 'API kjører med Helmet-beskyttelse' });
});

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

module.exports = app;

Start serveren og test hodene med curl:

npm run dev

# I et nytt terminalvindu:
curl -I http://localhost:3000/

# Forventet output:
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0

Du ser nå 11 sikkerhetshoder satt automatisk. Legg merke til at X-Powered-By: Express er fjernet. Helmet sletter dette hodet fordi det røper teknologi-stakken for angripere. X-XSS-Protection er satt til 0, altså deaktivert. Dette er bevisst: det gamle nettleser-XSS-filteret skapte egne sikkerhetssårbarheter i visse nettlesere og er i dag fullstendig erstattet av Content-Security-Policy.

Sammenlign med en app uten Helmet. Start en tom Express-server uten Helmet og test hodene:

# Uten Helmet:
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 42
Date: Tue, 17 Jun 2026 12:00:00 GMT
Connection: keep-alive

# Med Helmet:
HTTP/1.1 200 OK
Content-Security-Policy: [lang streng...]
Cross-Origin-Opener-Policy: same-origin
# ... 10 flere sikkerhetshoder
# X-Powered-By er IKKE med

Steg 4: Forstå alle standardhodene i Helmet 8.2.0

Helmet 8.2.0 setter følgende hoder som standard. Det er viktig å forstå hva hvert hode gjør og hvorfor standardverdien er valgt, slik at du kan ta gode beslutninger om tilpasning for din applikasjon.

HTTP-hodeStandardverdiAngrep blokkert
Content-Security-Policydefault-src ‘self’; (lang streng)XSS, data-injeksjon, ressursinjeksjon
Cross-Origin-Opener-Policysame-originSpectre-baserte sidekanalangrep
Cross-Origin-Resource-Policysame-originCross-origin ressurslekkasje
Origin-Agent-Cluster?1Prosesisolasjon på tvers av opphav
Referrer-Policyno-referrerURL-lekkasje via Referer-hode
Strict-Transport-Securitymax-age=15552000; includeSubDomainsSSL-stripping, nedgraderingsangrep
X-Content-Type-OptionsnosniffMIME-sniffing-angrep
X-DNS-Prefetch-ControloffDNS-basert informasjonslekkasje
X-Download-OptionsnoopenKjøring av nedlastede filer i IE
X-Frame-OptionsSAMEORIGINClickjacking
X-Permitted-Cross-Domain-PoliciesnoneAdobe Flash/PDF cross-domain tilgang
X-Powered-By(fjernet)Teknologi-fingeravtrykk
X-XSS-Protection0 (deaktivert)Gammel XSS-filter deaktivert, erstattet av CSP

Viktig merknad om X-XSS-Protection: 0. Det kan virke kontraintuitivt å deaktivere et XSS-beskyttelse-hode, men det er riktig valg. Det gamle filteret i Internet Explorer og tidlige versjoner av Chrome og Safari introduserte egne reflekterte XSS-sårbarheter via “XSS Auditor”-funksjonen. Moderne nettlesere har allerede fjernet denne funksjonaliteten. Å sette X-XSS-Protection: 0 hindrer eldre nettlesere i å aktivere den problematiske filteret, mens moderne nettlesere ignorerer hodet helt. CSP er den rette erstatningen.

Se den offisielle Helmet.js-dokumentasjonen for fullstendig liste over konfigurasjonsalternativer for hvert enkelt hode.

Steg 5: Konfigurer Content-Security-Policy (CSP)

Content-Security-Policy er det kraftigste hodet Helmet setter. Det forteller nettleseren nøyaktig hvilke ressurser den har lov til å laste. En streng CSP stopper de fleste XSS-angrep selv om angriperen lykkes med å injisere skadelig HTML i siden din.

Slik fungerer det i praksis: Anta at en angriper finner en SQL-injeksjonssårbarhet i kommentarfeltet ditt og injiserer <script src="https://angriper.no/skadelig.js"></script>. Uten CSP kjører nettleseren dette skriptet. Med CSP og scriptSrc: ["'self'"] nekter nettleseren å laste skriptet fordi angriper.no ikke er på tilliste, og angrepet stopper der.

Standard CSP fra Helmet blokkerer alle ekstern-ressurser med default-src 'self'. For en typisk webapplikasjon med Google Fonts, CDN-skript og bilder fra eksterne tjenester, trenger du å tilpasse dette:

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
        styleSrc: ["'self'", "https:", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'", "https://fonts.gstatic.com"],
        connectSrc: ["'self'", "https://api.dinapp.no"],
        frameSrc: ["'none'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        upgradeInsecureRequests: [],
      },
    },
  })
);

Direktivene du trenger å kjenne til:

  • defaultSrc: Fallback for alle ressurstyper som ikke har eget direktiv. Sett alltid denne til minst 'self'.
  • scriptSrc: Tillatte opphav for JavaScript-filer. 'self' betyr kun samme domene. Legg til spesifikke CDN-er du bruker.
  • styleSrc: Tillatte opphav for CSS. 'unsafe-inline' er nødvendig for mange CSS-rammeverk, men bruk nonce i stedet der det er mulig.
  • imgSrc: Tillatte opphav for bilder. data: tillater base64-innebygde bilder som brukes av mange UI-biblioteker.
  • fontSrc: Tillatte opphav for skriftfiler. Google Fonts bruker fonts.gstatic.com for selve fontfilene og fonts.googleapis.com for CSS-pekere.
  • connectSrc: Tillatte opphav for fetch, XHR og WebSocket-tilkoblinger. Legg til alle API-endepunkter applikasjonen kaller.
  • frameSrc: Kontrollerer hvilke domener som kan lastes i iframes i din side. 'none' blokkerer alt.
  • objectSrc: Tillatte opphav for <object>, <embed> og <applet>-elementer. Sett til 'none' for alle moderne applikasjoner.
  • baseUri: Begrenser verdier i <base>-elementet. Sett til 'self' for å hindre base-tag-injeksjon.
  • formAction: Begrenser URL-er som skjemaer kan sendes til. Viktig for å hindre skjema-hijacking via XSS.
  • upgradeInsecureRequests: Ber nettleseren oppgradere HTTP-lenker til HTTPS automatisk. Aktiverer med tom array.

Se MDN CSP-dokumentasjonen for fullstendig liste over direktiver og gyldige verdier.

Steg 6: Konfigurer Strict-Transport-Security (HSTS)

HSTS forteller nettleseren at den alltid skal bruke HTTPS for ditt domene, selv om brukeren skriver http:// manuelt. Uten HSTS kan en aktiv nettverksangriper (for eksempel på en offentlig wifi-hotspot) gjennomføre et SSL-stripping-angrep: de ser HTTP-forespørselen fra brukeren, kobler til din HTTPS-server på vegne av brukeren, og videresender trafikken ukryptert tilbake til brukeren. Brukeren ser en usikret tilkobling uten at nettleseren advarer, fordi HTTP er det de ba om.

HSTS løser dette ved å cache instruksjonen i nettleseren. Etter første HTTPS-besøk med HSTS-hodet, vil nettleseren aldri mer forsøke HTTP-tilkobling til dette domenet i løpet av max-age-perioden, selv om brukeren aktivt skriver http://.

Helmet setter max-age=15552000 (180 dager) og includeSubDomains som standard. For produksjon anbefaler OWASP Secure Headers Project en max-age på minimum ett år (31536000 sekunder) og å inkludere preload:

app.use(
  helmet({
    strictTransportSecurity: {
      maxAge: 31536000,        // 1 år i sekunder
      includeSubDomains: true, // Gjelder alle underdomener
      preload: true,           // Tillat opptak i nettlesernes HSTS preload-lister
    },
  })
);

preload: true betyr at du kan søke om å få domenet ditt inn i nettlesernes HSTS preload-liste. Denne listen er innebygd i Chrome, Firefox, Safari og Edge. Domener på listen er hardkodet til HTTPS i alle nye nettleserinstallasjoner, selv før første besøk. Dette eliminerer “first visit”-problemet der HSTS ikke beskytter brukere som aldri har besøkt siden din via HTTPS.

Aktiver ikke HSTS med preload: true på testmiljøer eller domener du ikke kontrollerer fullt ut. Når et domene er i preload-listen, tvinger alle nettlesere HTTPS. Det kan ta måneder å bli fjernet fra listen. Sett HSTS kun på produksjonsdomenet, og kun etter at du har bekreftet at HTTPS fungerer for alle sider og underdomener.

Steg 7: X-Frame-Options og clickjacking-beskyttelse

Clickjacking-angrep fungerer ved å legge din side i en usynlig iframe på en angriper-kontrollert side. Angriperen setter iframens opasitet til null (usynlig) og plasserer den over et lokke-element. Når brukeren tror de klikker på for eksempel en “Vinn en iPhone”-knapp, klikker de faktisk på din applikasjons grensesnitt under. Angripere bruker dette til å stjele klikk på bekreftelsesknapper for betalinger, kontosletting, tillatelse til kamera/mikrofon, og deling av sensitiv informasjon.

Helmet setter X-Frame-Options: SAMEORIGIN som standard, som betyr at kun sider fra samme opphav kan laste inn siden din i en iframe. For de fleste applikasjoner bør du sette dette til DENY for å hindre all inlasting i iframes:

app.use(
  helmet({
    frameguard: {
      action: 'deny', // Ingen kan frame siden din
      // Alternativ: 'sameorigin' (standard) - kun eget domene kan frame
    },
    contentSecurityPolicy: {
      directives: {
        // frame-ancestors erstatter X-Frame-Options i moderne nettlesere
        frameAncestors: ["'none'"],
        defaultSrc: ["'self'"],
        objectSrc: ["'none'"],
      },
    },
  })
);

Merk at X-Frame-Options er et eldre hode som ikke støtter hvitelisting av spesifikke domener. Vil du tillate framing kun fra et spesifikt partner-domene, bruk frame-ancestors-direktivet i CSP: frameAncestors: ["https://partner.no"]. Bruk begge hodene for bred nettleser-kompatibilitet ned til eldre versjoner av Internet Explorer og Safari.

Steg 8: X-Content-Type-Options og Cross-Origin-sikkerhet

MIME-sniffing skjer når nettleseren ignorerer den deklarerte Content-Type og prøver å gjette filtypen basert på innholdet. En angriper kan laste opp en fil med endelsen .jpg, men med JavaScript-innhold. Eldre nettlesere uten MIME-sniffing-beskyttelse kjører denne filen som JavaScript. Helmet setter X-Content-Type-Options: nosniff automatisk.

Sørg alltid for at Express sender korrekte Content-Type-hoder. Express setter application/json automatisk for res.json() og text/html for res.send() med strenger. Unngå å sende rådata uten eksplisitt Content-Type.

To nyere hoder i Helmet gir beskyttelse mot sofistikerte sidekanalangrep basert på Spectre-prosessor-sårbarheten:

Cross-Origin-Resource-Policy: same-origin hindrer andre nettsteder i å laste ressursene dine (bilder, skript, data) via fetch eller XMLHttpRequest. Uten dette hodet kan en ondsinnet nettside laste inn bilder fra din autentiserte applikasjon og bruke Spectre-timing-angrep for å lese piksel-dataene fra nettleserminnet.

Cross-Origin-Opener-Policy: same-origin isolerer nettleservinduet ditt fra andre vinduer og tabs. Det hindrer angripere i å holde en referanse til vinduet ditt etter navigasjon, noe som er en forutsetning for visse timer-baserte sidekanalangrep og window.opener-baserte angrep.

app.use(
  helmet({
    noSniff: true,  // Standard, trenger ikke sette eksplisitt
    crossOriginResourcePolicy: {
      policy: 'same-origin', // Alternativ: 'cross-origin' for åpne ressurser
    },
    crossOriginOpenerPolicy: {
      policy: 'same-origin', // Alternativ: 'same-origin-allow-popups' for popup-apper
    },
  })
);

Steg 9: Referrer-Policy og informasjonslekkasje

Nettleseren sender som standard Referer-hodet med full URL når brukeren klikker på en lenke eller sender en forespørsel. Dette kan lekke sensitive URL-parametere til tredjepartstjenester. Et praktisk eksempel: brukeren klikker på en lenke fra en side med URL https://dinapp.no/tilbakestill-passord?token=abc123. Dersom siden inkluderer en tredjepartsfont, script eller sporingspiksel, mottar tredjepartens server dette token via Referer-hodet, og en angriper som kontrollerer den tjenesten kan bruke tokenet til å tilbakestille brukerens passord.

Helmet setter no-referrer som standard. For applikasjoner som trenger Referrer-informasjon for analyseverktøy (for å spore hvilke sider brukere kommer fra), bruk strict-origin-when-cross-origin:

app.use(
  helmet({
    referrerPolicy: {
      // 'no-referrer' (standard): Send ingen referrer-info til noen
      // 'strict-origin': Send kun domene (ikke sti) til andre opphav
      // 'strict-origin-when-cross-origin': Full URL til samme opphav,
      //   kun domene til andre opphav via HTTPS
      // 'origin-when-cross-origin': Full URL internt, kun domene eksternt
      policy: 'strict-origin-when-cross-origin',
    },
  })
);

Velg politikk basert på sensitiviteten til URL-parametere i applikasjonen din. For bankapplikasjoner, helsetjenester og apper med engangstokener i URL-er: bruk alltid no-referrer eller strict-origin. For e-handelssider med analyse-behov: strict-origin-when-cross-origin er en god balanse.

Steg 10: Nonce-basert CSP for inline-skript

Mange applikasjoner trenger inline-JavaScript i HTML-sider, for eksempel for å injisere konfigurasjonsdata fra serveren: applikasjonsnavn, bruker-ID, funksjonsflagg og liknende. Med en streng CSP som blokkerer 'unsafe-inline', feiler disse skriptene. Løsningen er nonce-basert CSP.

En CSP-nonce er en tilfeldig base64-streng som genereres av serveren for hvert HTTP-svar. Serveren setter 'nonce-ABC123' i CSP-hodet, der ABC123 er noncen. I HTML-en legger du nonce="ABC123"<script>– og <style>-elementer. Nettleseren tillater kun kjøring av elementer der nonce-attributtet matcher CSP-hodesverdien. En angriper som injiserer <script>ondsinnet()</script> får ikke skriptet til å kjøre fordi de ikke kjenner noncen for dette spesifikke svaret.

const crypto = require('crypto');
const express = require('express');
const helmet = require('helmet');

const app = express();

// Generer unik nonce for hvert svar
app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

// Bruk nonce i CSP - ny Helmet-instans per svar
app.use((req, res, next) => {
  return helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
        styleSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`, "https:"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        upgradeInsecureRequests: [],
      },
    },
  })(req, res, next);
});

// HTML-rute som bruker nonce i skript og stil
app.get('/', (req, res) => {
  const brukerKonfig = { versjon: '1.0.0', env: 'produksjon' };

  res.send(`
    
    
      
        
        Sikker app
        
      
      
        

Sikker Node.js-app med Helmet.js

Applikasjonen kjører med full Helmet-beskyttelse.

`); }); app.listen(3000);

Nonce-verdien skal være unik for hvert HTTP-svar og aldri gjenbrukes. crypto.randomBytes(16) gir 128 bits entropi, mer enn nok til å gjøre brute-force-gjetting praktisk umulig. Merk at dette betyr at du ikke kan bruke app.use(helmet(...)) én gang globalt med nonce-verdier, fordi nonce-verdien er ukjent på det tidspunktet. Løsningen er å opprette en ny Helmet-instans per svar via en mellomvare-fabrikk, som vist i kodeeksempelet over.

Steg 11: Rutebasert overstyring av Helmet-konfigurasjon

Ulike ruter kan trenge ulike sikkerhetshoder. Et offentlig API som serverer data til tredjeparts klienter trenger en annen CORP-innstilling enn private ruter. Et dashboard som laster inn tredjeparts widgets trenger bredere CSP-regler enn en innloggingsside.

Express sin mellomvare-kjeding gjør dette mulig. Registrer en global Helmet-konfigurasjon for de fleste ruter, og overstyr den for spesifikke ruter:

const express = require('express');
const helmet = require('helmet');

const app = express();

// Global Helmet-konfigurasjon for alle ruter
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      objectSrc: ["'none'"],
    },
  },
  frameguard: { action: 'deny' },
}));

// Dashboard-rute som trenger ekstern widget-CDN
const dashboardHelmet = helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "https://cdn.widget-leverandor.no"],
      frameSrc: ["https://widget-leverandor.no"],
      imgSrc: ["'self'", "https://widget-leverandor.no", "data:"],
      objectSrc: ["'none'"],
    },
  },
  frameguard: { action: 'sameorigin' }, // Tillat framing fra eget domene
});

// Helmet-instanser i rute-definisjoner kjøres ETTER global Helmet
// Det siste hodet vinner - test alltid med curl -I
app.get('/dashboard', dashboardHelmet, (req, res) => {
  res.json({ side: 'dashboard', tillatteWidgets: true });
});

// Offentlig API-rute der andre domener skal hente data
const offentligApiHelmet = helmet({
  crossOriginResourcePolicy: {
    policy: 'cross-origin', // Tillat cross-origin tilgang til denne ressursen
  },
});

app.get('/api/v1/offentlig', offentligApiHelmet, (req, res) => {
  res.json({ data: 'tilgjengelig for alle opphav' });
});

// Private API-ruter bruker global Helmet (same-origin CORP som standard)
app.get('/api/v1/privat', (req, res) => {
  res.json({ brukerdata: 'kun tilgjengelig fra eget opphav' });
});

Steg 12: Test og verifiser alle sikkerhetshoder

Testing er like viktig som konfigurasjon. Feil konfigurasjon som passerer usett er verre enn ingen konfigurasjon, fordi den gir falsk trygghet. Det finnes tre testmetoder: manuell med curl, automatisert med kode, og nettbaserte skannere.

Metode 1: Manuell test med curl

# Vis alle svar-hoder
curl -I http://localhost:3000/

# Filtrer de viktigste sikkerhetshoder
curl -s -I http://localhost:3000/ | grep -iE "(content-security|x-frame|strict-transport|x-content-type|referrer|cross-origin)"

# Bekreft at X-Powered-By er fjernet (ingen output = fjernet)
curl -s -I http://localhost:3000/ | grep -i "x-powered-by"

# Test at X-XSS-Protection er satt til 0
curl -s -I http://localhost:3000/ | grep -i "x-xss-protection"
# X-XSS-Protection: 0

# Test HSTS i produksjon (krever HTTPS)
curl -s -I https://dinapp.no/ | grep -i "strict-transport-security"
# Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Metode 2: Automatisert test i kode

const assert = require('assert');
const http = require('http');
const app = require('./src/app');

let server;

before((done) => {
  server = app.listen(3001, done);
});

after((done) => {
  server.close(done);
});

async function testSikkerhetshoder() {
  return new Promise((resolve, reject) => {
    http.get('http://localhost:3001/', (res) => {
      const hoder = res.headers;

      // CSP skal være satt
      assert.ok(hoder['content-security-policy'], 'FEIL: CSP mangler');
      assert.ok(
        hoder['content-security-policy'].includes("default-src 'self'"),
        'FEIL: CSP mangler default-src self'
      );

      // HSTS skal være satt
      assert.ok(hoder['strict-transport-security'], 'FEIL: HSTS mangler');

      // X-Powered-By skal IKKE være satt
      assert.ok(!hoder['x-powered-by'], 'FEIL: X-Powered-By er ikke fjernet');

      // X-Frame-Options skal være satt
      assert.ok(hoder['x-frame-options'], 'FEIL: X-Frame-Options mangler');

      // X-Content-Type-Options skal være nosniff
      assert.strictEqual(
        hoder['x-content-type-options'],
        'nosniff',
        'FEIL: X-Content-Type-Options er ikke nosniff'
      );

      // Referrer-Policy skal være satt
      assert.ok(hoder['referrer-policy'], 'FEIL: Referrer-Policy mangler');

      // Cross-Origin-Resource-Policy skal være satt
      assert.ok(hoder['cross-origin-resource-policy'], 'FEIL: CORP mangler');

      console.log('Alle sikkerhetshoder verifisert ✓');
      resolve();
    }).on('error', reject);
  });
}

testSikkerhetshoder().catch((err) => {
  console.error('Sikkerhetstest feilet:', err.message);
  process.exit(1);
});

Metode 3: Nettbasert skanner. For produksjonstesting: skann nettadressen din med securityheaders.com. Tjenesten gir en karakter fra A+ til F. En ny Express-app uten Helmet scorer typisk F. Med korrekt Helmet-konfigurasjon scorer du A eller A+. Mozilla Observatory (observatory.mozilla.org) er et annet godt alternativ som sjekker enda flere sikkerhetsindikatorer.

Komplett prosjekt: Produksjonsklar sikker Express-app

Her er et komplett, produksjonsklart eksempel som kombinerer alle stegene. Denne koden er egnet som utgangspunkt for alle nye Express-prosjekter med norske brukere eller virksomheter underlagt NIS2/Digitalsikkerhetsloven.

'use strict';

const crypto = require('crypto');
const express = require('express');
const helmet = require('helmet');

const app = express();
const isProd = process.env.NODE_ENV === 'production';

// Grense på forespørsels-kropper for å hindre DoS
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// Generer CSP-nonce for hvert svar
app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

// Helmet-konfigurasjon
app.use((req, res, next) => {
  return helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
        styleSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`, "https:"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'", "https:", "data:"],
        connectSrc: ["'self'"],
        frameSrc: ["'none'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        frameAncestors: ["'none'"],
        upgradeInsecureRequests: isProd ? [] : null,
        // Samle brudd-rapporter i produksjon:
        ...(isProd && { reportUri: ['/api/csp-rapport'] }),
      },
      reportOnly: !isProd, // Test CSP i rapport-modus i utvikling
    },

    // HSTS kun i produksjon
    strictTransportSecurity: isProd
      ? { maxAge: 31536000, includeSubDomains: true, preload: true }
      : false,

    frameguard: { action: 'deny' },
    crossOriginOpenerPolicy: { policy: 'same-origin' },
    crossOriginResourcePolicy: { policy: 'same-origin' },
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
    noSniff: true,
    dnsPrefetchControl: { allow: false },
    hidePoweredBy: true,
  })(req, res, next);
});

// Helse-endepunkt
app.get('/helse', (req, res) => {
  res.json({ status: 'ok', tidspunkt: new Date().toISOString() });
});

// API-endepunkt
app.get('/api/v1/data', (req, res) => {
  res.json({
    melding: 'Sikret med Helmet.js 8.2.0',
    miljo: process.env.NODE_ENV || 'utvikling',
  });
});

// Motta CSP-brudd-rapporter
if (isProd) {
  app.post(
    '/api/csp-rapport',
    express.json({ type: 'application/csp-report' }),
    (req, res) => {
      console.warn('CSP-brudd:', JSON.stringify(req.body));
      res.status(204).end();
    }
  );
}

// Global feilhåndtering - ingen stack trace til klienten
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ feil: 'Intern serverfeil' });
});

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

module.exports = app;

Helmet.js og OWASP: sikkerhetskontrollist

OWASP Security Headers Project definerer et standardsett med sikkerhetshoder og anbefaler konfigurasjonsverdier for hvert. Tabellen nedenfor viser hvordan Helmets standardkonfigurasjon dekker OWASP-anbefalingene, og hva du bør justere for full samsvar.

OWASP-anbefalingHelmet standardBehov for justering?Anbefalt justering
CSP med strict-dynamic eller nonceGrunnleggende CSPJaBruk nonce-basert CSP (steg 10)
HSTS max-age >= 1 år180 dager (15552000)Ja i produksjonSett maxAge: 31536000
HSTS preloadIkke satt som standardJa for produksjonLegg til preload: true
X-Frame-Options DENYSAMEORIGINFor de fleste apperSett action: ‘deny’
Referrer-Policy strict-originno-referrerValgfrittno-referrer er strengere, OK
CORP same-originsame-originNeiStandard er korrekt
Fjern X-Powered-ByFjernet automatiskNeiStandard er korrekt

For norske virksomheter underlagt NIS2 gjelder kravene i Digitalsikkerhetsloven om å implementere egnede tekniske tiltak for å beskytte nettverks- og informasjonssystemer. Korrekt satte HTTP-sikkerhetshoder er et grunnleggende og veldokumentert tiltak som er lett å verifisere og auditere.

Vanlige feil og fallgruver

Disse feilene dukker opp igjen og igjen i produksjons-implementasjoner av Helmet:

Feil 1: Helmet etter ruter i mellomvare-stabelen. Express kjører mellomvare i den rekkefølgen du registrerer den. Registrerer du app.use(helmet()) etter app.use(router), setter ikke Helmet hoder på svar fra ruteren. Plasser alltid Helmet øverst i mellomvare-stabelen, rett etter parsing av kropp og logging. Kontroller med curl at hodene faktisk finnes i svarene fra ruter-endepunktene dine, ikke bare fra rot-ruten.

Feil 2: CSP bryter tredjepartsintegrasjoner. Den vanligste årsaken til at Helmet “ødelegger” ting er en for streng CSP. Kartlegg alle eksterne ressurser applikasjonen laster (skript, stiler, fonter, bilder, API-kall, WebSocket-tilkoblinger) og legg dem til i CSP-direktivene. Bruk nettleserens konsoll under utvikling for å se CSP-brudd i sanntid.

Feil 3: HSTS på testdomener. Aktiverer du HSTS med høy max-age på et testdomene, låser du dette domenet til HTTPS i alle nettlesere som besøker det. Bruk betingelser: aktiver HSTS kun i produksjon via process.env.NODE_ENV === 'production'. Har du gjort denne feilen, kan du nullstille ved å sette max-age=0 og la brukere besøke siden via HTTPS med det hodet, men dette tar tid.

Feil 4: Gjenbruk av CSP-nonce. Nonce-verdien MÅ genereres på nytt for hvert HTTP-svar. Lagrer du nonce som en global variabel eller i express-sesjon og gjenbruker den for alle svar, kan en angriper stjele nonce-verdien fra ett svar og bruke den til å kjøre injiserte skript i et annet svar. Bruk alltid crypto.randomBytes() per forespørsel.

Feil 5: CORP blokkerer offentlig API. Cross-Origin-Resource-Policy: same-origin er riktig for private ressurser, men bryter offentlige API-er som skal kalles fra andre domener. Sett crossOriginResourcePolicy: { policy: 'cross-origin' } for API-endepunkter som skal være åpne for alle opphav.

Feil 6: For bred scriptSrc med unsafe-eval. Mange setter 'unsafe-eval' i scriptSrc fordi et bibliotek krever det (typisk eldre versjoner av Angular eller template engines). 'unsafe-eval' tillater bruk av eval(), Function(), setTimeout/setInterval med strenger, og liknende. Dette er en av de farligste CSP-direktiv-verdiene fordi den åpner for kodeinjeksjon. Oppgrader biblioteket eller finn et alternativ fremfor å akseptere 'unsafe-eval'.

Feilsøking: 8 symptomer med løsninger

SymptomÅrsakLøsning
Google Fonts lastes ikke innfontSrc mangler fonts.gstatic.com og fonts.googleapis.comLegg til begge domenene i fontSrc og styleSrc
Bilder fra CDN vises ikkeimgSrc mangler CDN-domenetLegg CDN-domenet til imgSrc
Inline-skript blokkert av CSPCSP mangler nonce eller ‘unsafe-inline’Bruk nonce-basert CSP fra steg 10
CORS-feil etter HelmetCORP-hode blokkerer cross-origin-kallSett crossOriginResourcePolicy: ‘cross-origin’ for åpne endepunkter
Innlogging via iframe feilerX-Frame-Options DENY blokkerer innlogging i widgetSett frameguard: { action: ‘sameorigin’ } for den ruten
HSTS låser ut testmiljø på HTTPHSTS aktivert i ikke-produksjonsmiljøBetinget aktivering: process.env.NODE_ENV === ‘production’
PDF-innlasting via Adobe feilerX-Permitted-Cross-Domain-Policies: noneSett permittedCrossDomainPolicies: { permittedPolicies: ‘master-only’ }
WebSocket-tilkobling feilerconnectSrc mangler wss://Legg til “wss://din-server.no” i connectSrc-direktivet

Når du feilsøker CSP-problemer, åpne nettleserens utviklerverktøy (F12) og se i konsollen. Nettleseren gir presise feilmeldinger som forteller nøyaktig hvilken ressurs som ble blokkert:

# Typisk CSP-feilmelding i Chrome-konsollen:
Refused to load the script 'https://eksempel.no/skript.js' because it violates
the following Content Security Policy directive: "script-src 'self'".
Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as fallback.

# Løsning: Legg til domenet i scriptSrc-direktivet:
scriptSrc: ["'self'", "https://eksempel.no"],

# CSP-brudd for font:
Refused to load the font 'https://fonts.gstatic.com/s/roboto/v30/...' because
it violates the following Content Security Policy directive: "font-src 'self'".

# Løsning:
fontSrc: ["'self'", "https://fonts.gstatic.com"],

Avanserte tips for produksjonsmiljøer

1. CSP-rapportering i produksjon. Bruk report-uri eller report-to-direktivet for å samle inn CSP-brudd-rapporter fra ekte brukere. Dette gir synlighet i injeksjonsforsøk og konfigurasjonsfeil du aldri ser i testmiljøet. En angriper som prøver XSS vil generere brudd-rapporter som du kan bruke til å oppdage og respondere på angrepsforsøk:

contentSecurityPolicy: {
  directives: {
    defaultSrc: ["'self'"],
    reportUri: ['/api/csp-rapport'], // Motta brudd-rapporter
  },
  reportOnly: !isProd, // I utvikling: kun rapporter, ikke blokker
},

// Endepunkt for å motta og logge brudd-rapporter
app.post('/api/csp-rapport',
  express.json({ type: 'application/csp-report' }),
  (req, res) => {
    const rapport = req.body['csp-report'];
    if (rapport) {
      console.warn('CSP-brudd oppdaget:', {
        blokkertUri: rapport['blocked-uri'],
        bruddDirectiv: rapport['violated-directive'],
        kildeFil: rapport['source-file'],
        linjenummer: rapport['line-number'],
      });
      // Send til logg-system (f.eks. Elasticsearch, Datadog)
    }
    res.status(204).end();
  }
);

2. Bruk CSP i report-only modus ved overgang til ny CSP. Når du strammer inn CSP-reglene på et eksisterende system, skru på reportOnly: true i to til fire uker. Analyser brudd-rapportene, legg til legitime kilder i direktivene, og skru deretter av report-only modus. Dette unngår produksjonsbrudd for reelle brukere.

3. Kombiner Helmet med rate limiting. Helmet beskytter mot header-baserte angrep, men ikke mot brute-force eller DoS. Kombiner alltid med rate limiting på autentiserings-endepunkter. Se vår guide om HTTPS og TLS 1.3 i Node.js for konfigurering av sikker transportlag.

4. Automatisk oppdatering av Helmet. Helmet-teamet oppdaterer standardverdier basert på endringer i nettleser-støtte og nye sikkerhetsstandarder. Kjør npm outdated ukentlig og oppdater Helmet ved nye versjoner. Bruk Dependabot eller Renovate for automatiske oppdaterings-PR-er. Les changelog ved oppgradering fra 7.x til 8.x: det ble endringer i standardverdier for CORP og COOP. Se Helmet GitHub-repositoriet for full changelog.

5. Kombiner med passordhashing på database-laget. Helmet gir nettverkslag-beskyttelse. Passordhashing på database-laget er like viktig. Les mer i vår guide om scrypt Passordhashing i Node.js for moderne, minnehard passordhashing.

6. Deployment-sjekkliste. Gå gjennom disse punktene før du setter Helmet i produksjon:

  • Bekreft med curl at alle 11 sikkerhetshoder er satt på alle endepunkter.
  • Verifiser at X-Powered-By er fjernet.
  • Test at CSP ikke blokkerer noe som brukerne trenger (fonter, bilder, API-kall).
  • Aktiver CSP i report-only modus i minst én uke i produksjon og analyser rapporter.
  • Bekreft at HSTS kun er aktivert for produksjonsdomenet med gyldig HTTPS-sertifikat.
  • Sjekk securityheaders.com og sørg for A eller A+ karakter.
  • Dokumenter CSP-reglene og begrunn alle ikke-standard direktiver.

Relatert dekning

Utdyp sikkerhetskunnskapen din med disse artiklene fra shattered.io:

Ofte stilte spørsmål

Trenger jeg Helmet.js hvis jeg bruker en omvendt proxy som Nginx?

Ja. Nginx kan sette noen statiske sikkerhetshoder via add_header-direktiver, men Helmet gir granulær kontroll per rute i Express og støtter dynamiske verdier som nonce-basert CSP. CSP-regler med nonce krever generering per svar, noe Nginx ikke kan gjøre uten Lua-moduler og mye ekstra konfigurasjon. Den vanlige praksisen er å bruke Nginx for HSTS og grunnleggende statiske hoder, og Helmet for CSP og applikasjonsspesifikke hoder der dynamikk er nødvendig.

Er Helmet.js kompatibelt med Express 5?

Ja. Helmet 8.2.0 støtter både Express 4.x og 5.x. Mellomvare-API-et er identisk. Oppgrader Express og Helmet separat og kjør testene etter hvert steg for å minimere risiko.

Hva med Fastify i stedet for Express?

Helmet er skrevet for Express. For Fastify bruker du @fastify/helmet, som er en offisiell wrapper rundt Helmet med Fastify-kompatibelt API. Installasjon med npm install @fastify/helmet og registrering med await fastify.register(require('@fastify/helmet')). Konfigurasjonssyntaksen er nesten identisk med Express-varianten.

Kan Helmet erstatte en Web Application Firewall (WAF)?

Nei. Helmet setter sikkerhetshoder som påvirker nettleser-oppførsel for utgående svar. En WAF filtrerer innkommende trafikk og blokkerer angrep på nettverkslaget, for eksempel SQL-injeksjon, path traversal og volumetriske angrep. De to løsningene utfyller hverandre og adresserer ulike angrepsvektorer. For produksjon bruker du begge.

Virker Helmet for REST-APIer uten nettleser-klienter?

Delvis. CSP og X-Frame-Options gir lite verdi for maskin-til-maskin-APIer uten nettleser-klient. Men HSTS, X-Content-Type-Options og CORP gir fortsatt verdi. Fjern hoder som ikke er relevante med for eksempel contentSecurityPolicy: false og frameguard: false for rene API-servere uten nettleser-klienter.

Påvirker Helmet ytelsen merkbart?

Negligerbart. Helmet setter hoder i JavaScript-minnet, uten I/O-operasjoner eller tunge beregninger (med unntak av nonce-generering med crypto.randomBytes(16)). Overhead er under 0,1 millisekund per forespørsel på typisk servermaskinvare. Ytelsespåvirkning er ikke en grunn til å utelate Helmet.

Hvilket hode gir mest sikkerhet?

Content-Security-Policy. En korrekt konfigurert CSP stopper de aller fleste XSS-angrep, selv om angriperen klarer å injisere skadelig HTML. XSS er konsekvent blant de 10 mest utbredte sårbarhetstypene ifølge OWASP Secure Headers Project. CSP er den mest effektive header-baserte forsvarslinjen mot denne trusselen.

Hva er et godt utgangspunkt for en ny CSP?

Start med Helmets standard, aktiver reportOnly: true, og deploy til et testmiljø med reell trafikk. La brudd-rapportene samle seg i en til to uker. Analyser dem, legg til legitime domener og ressurser i CSP-direktivene dine, og test igjen. Gjenta til ingen nye legitime brudd dukker opp, og skru deretter av report-only modus for å aktivere faktisk blokkering.

Finnes det et verktøy for å automatisk generere CSP for eksisterende apper?

Ja. csp-header-npm-pakken og nettleserutvidelsen “CSP Evaluator” fra Google kan hjelpe med å identifisere nødvendige direktiver. Den beste metoden er likevel å aktivere reportOnly: true i Helmet og la brudd-rapportene fra ekte brukere fortelle deg nøyaktig hvilke ressurser applikasjonen laster. Dette gir den mest nøyaktige CSP for akkurat din applikasjon og dine brukeres nettlesere.