OWASP Top 10 är den mest citerade säkerhetsstandarden för webbapplikationer. Den senaste versionen, OWASP Top 10:2025, publicerades i november 2025 och identifierar tio kritiska sårbarhetskategorier som drabbar virtuellt alla testade applikationer. I den här guiden implementerar du skydd mot samtliga tio kategorier i en Node.js/Express.js-applikation, steg för steg, på 30 minuter.
Konsekvenserna av att ignorera OWASP är mätbara: Broken Access Control, som toppar listan, finns i nära 100 procent av testade applikationer. Security Misconfiguration (A02:2025) rapporterades i samtliga testade appar. Cryptographic Failures (A04:2025) har kartlagts i över 1,6 miljoner förekomster i offentliga databaser. Den här handledningen ger dig ett komplett Node.js-projekt med fungerande motåtgärder mot varje kategori.
Vad är OWASP Top 10:2025?
OWASP (Open Worldwide Application Security Project) är en ideell organisation som publicerar öppen säkerhetsforskning. Deras Top 10-lista är en bred sammanställning av de vanligaste och farligaste säkerhetsriskerna mot webbapplikationer, baserad på bidragen från hundratals organisationer och miljontals testade applikationer.
OWASP Top 10:2025 är en uppdatering av 2021 års lista. Tre kategorier är nya eller kraftigt omdefinierade: Security Misconfiguration klättrade från plats fem till plats två, Software Supply Chain Failures (A03:2025) är en ny kategori som ersätter “Using Components with Known Vulnerabilities”, och Injection sjönk till femte plats trots att den fortfarande finns i 100 procent av testade applikationer.
| Plats | Kategori | Förekomst i testade appar | Antal kartlagda CWE |
|---|---|---|---|
| A01 | Broken Access Control | ~100% | Högst antal av alla kategorier |
| A02 | Security Misconfiguration | 100% | 719 000+ |
| A03 | Software Supply Chain Failures | 5,19% (genomsnittlig incidens) | 215 000+ förekomster |
| A04 | Cryptographic Failures | Hög | 1 600 000+ förekomster |
| A05 | Injection | 100% | Flest CVE:er av alla kategorier |
| A06 | Vulnerable and Outdated Components | Hög | Stor |
| A07 | Identification and Authentication Failures | Hög | Stor |
| A08 | Software and Data Integrity Failures | Medel | Medel |
| A09 | Security Logging and Monitoring Failures | Hög | Stor |
| A10 | Server-Side Request Forgery (SSRF) | Ökande | Medel |
Förutsättningar
Innan du startar behöver du följande installerat och konfigurerat:
- Node.js 22.x LTS (minst 20.x, äldre versioner som 17.x och 19.x har kritiska CVE:er som CVE-2025-23087 och CVE-2025-23088)
- npm 10.x eller yarn 4.x
- Express.js 4.21.x eller 5.x
- En texteditor (VS Code rekommenderas)
- Terminal med bash eller zsh
- Grundläggande kunskaper i JavaScript och HTTP
Kontrollera din Node.js-version med:
node --version
npm --version
Om du kör Node.js 20.x eller äldre, uppgradera innan du fortsätter. I januari 2026 patchade Node.js-teamet åtta sårbarheter i alla aktiva utgåvor, varav tre klassades som hög allvarlighetsgrad. CVE-2025-55131 exponerade oinitaliserat heap-minne via Buffer.alloc() efter vm-timeout, vilket potentiellt läcker lösenord och API-tokens i minnet.
Projektstruktur och installation
Skapa ett nytt projekt och installera de säkerhetspaket du behöver för att täcka hela OWASP Top 10:
mkdir owasp-nodejs-demo && cd owasp-nodejs-demo
npm init -y
npm install express helmet express-rate-limit express-validator \
bcrypt jsonwebtoken dotenv cors cookie-parser winston \
express-mongo-sanitize hpp csurf
npm install --save-dev nodemon snyk
Paketen täcker följande OWASP-kategorier:
| Paket | Version (juni 2026) | OWASP-kategori |
|---|---|---|
| helmet | 8.x | A02: Security Misconfiguration |
| express-rate-limit | 7.x | A07: Authentication Failures |
| express-validator | 7.x | A05: Injection |
| bcrypt | 5.x | A04: Cryptographic Failures |
| jsonwebtoken | 9.x | A07: Authentication Failures |
| winston | 3.x | A09: Logging and Monitoring Failures |
| express-mongo-sanitize | 2.x | A05: Injection (NoSQL) |
| hpp | 0.2.x | A05: HTTP Parameter Pollution |
Steg 1: Grundkonfiguration med helmet (A02)
Security Misconfiguration klättrade till andraplatsen i OWASP Top 10:2025. Det inkluderar osäkra HTTP-headers, öppna felmeddelanden och standardkonfigurationer som lämnar känslig information exponerad. I Node.js/Express skickas som standard headers som X-Powered-By: Express, vilket berättar för angripare vilken teknikstack du använder.
Skapa filen app.js och börja med helmet:
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const app = express();
// A02: Security Misconfiguration - säkra HTTP-headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000, // 1 år i sekunder
includeSubDomains: true,
preload: true,
},
noSniff: true,
xssFilter: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
// Ta bort X-Powered-By helt
app.disable('x-powered-by');
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
module.exports = app;
Helmet sätter åtta säkerhetskritiska headers automatiskt: Content-Security-Policy, X-DNS-Prefetch-Control, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, X-XSS-Protection, Referrer-Policy och Permissions-Policy. Content-Security-Policy (CSP) är det viktigaste skyddet mot XSS-attacker och begränsar vilka resurser webbläsaren får ladda.
Begränsningen av request-storlek till 10 KB skyddar mot “JSON bomb”-attacker där en angripare skickar djupt nestade JSON-strukturer som tar enorma mängder minne att parsa. Express har inget standardgränsvärde, vilket är ett vanligt konfigurationsmisstag.
Steg 2: Broken Access Control (A01)
Broken Access Control är den vanligaste kategorin i OWASP Top 10:2025 och finns i nästan alla testade applikationer. Det innebär att användare kan utföra åtgärder eller komma åt data utanför sina behörigheter. Typiska fel är att ID-parametrar i URL:er kan manipuleras (Insecure Direct Object Reference, IDOR), att rollkontroller saknas på API-nivå, eller att känsliga endpoints inte skyddas alls.
Skapa middleware för rolbaserad åtkomstkontroll i middleware/auth.js:
const jwt = require('jsonwebtoken');
// Verifiera JWT-token
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Autentisering krävs' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'Ogiltig eller utgången token' });
}
};
// Kontrollera rollbehörigheter
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Behörighet saknas' });
}
next();
};
};
// Förhindra IDOR: verifiera att användaren äger resursen
const checkResourceOwnership = (getOwnerId) => {
return async (req, res, next) => {
try {
const ownerId = await getOwnerId(req);
if (String(ownerId) !== String(req.user.id)) {
return res.status(403).json({ error: 'Åtkomst nekad' });
}
next();
} catch (err) {
next(err);
}
};
};
module.exports = { authenticate, authorize, checkResourceOwnership };
Nyckelprincipen är att implementera “deny by default”: alla endpoints kräver autentisering som standard, och specifika roller beviljas explicit. Utan denna princip riskerar du att en ny endpoint publiceras utan skydd. IDOR-skyddet i checkResourceOwnership säkerställer att en inloggad användare inte kan läsa eller modifiera en annan användares data bara genom att byta ID i URL:en.
Steg 3: Inputvalidering mot Injection (A05)
Injection är OWASP A05:2025 och finns i 100 procent av testade applikationer. Det inkluderar SQL-injektion, NoSQL-injektion, OS-kommandoinjektion och LDAP-injektion. I Node.js-applikationer med MongoDB är NoSQL-injektion lika allvarlig som SQL-injektion och missas ofta av utvecklare som tror att “ingen SQL = inget injektionsproblem”.
Skapa valideringsregler i validators/userValidator.js:
const { body, param, validationResult } = require('express-validator');
const mongoSanitize = require('express-mongo-sanitize');
const hpp = require('hpp');
// Middleware: sanitera MongoDB-operatorer och HTTP-parameterförorening
const sanitizeInput = [
mongoSanitize(), // tar bort $ och . från req.body, req.query, req.params
hpp(), // tar bort duplicerade query-parametrar
];
// Valideringsregler för registrering
const registerValidation = [
body('email')
.isEmail().withMessage('Ogiltig e-postadress')
.normalizeEmail()
.isLength({ max: 254 }).withMessage('E-postadress för lång'),
body('password')
.isLength({ min: 12, max: 128 }).withMessage('Lösenord: 12–128 tecken')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
.withMessage('Lösenord måste innehålla stora/små bokstäver, siffra och specialtecken'),
body('username')
.trim()
.isAlphanumeric('sv-SE').withMessage('Användarnamn: bokstäver och siffror')
.isLength({ min: 3, max: 30 }).withMessage('Användarnamn: 3–30 tecken')
.escape(),
];
// Hantera valideringsfel
const handleValidation = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Valideringsfel',
details: errors.array().map(e => ({ field: e.path, message: e.msg })),
});
}
next();
};
// Blockera farliga tecken i sökparametrar
const searchValidation = [
param('query')
.trim()
.escape()
.isLength({ max: 100 }).withMessage('Sökning för lång'),
];
module.exports = {
sanitizeInput,
registerValidation,
handleValidation,
searchValidation,
};
Express-mongo-sanitize tar bort MongoDB-operatörer ($ och .) från indata, vilket förhindrar attacker som {"$gt": ""} i lösenordsfältet som annars returnerar alla användare. HPP (HTTP Parameter Pollution) skyddar mot att en angripare skickar duplicerade parametrar som ?role=user&role=admin och därmed kringgår rollkontroller.
Steg 4: Säker lösenordshantering och kryptografisk styrka (A04)
Cryptographic Failures (A04:2025) har kartlagts i över 1,6 miljoner förekomster. Det inkluderar svaga hashfunktioner för lösenord (MD5, SHA-1 utan saltning), osäker lagring av känslig data, och dålig nyckelhantering. I Node.js är det vanligaste misstaget att använda crypto.createHash('md5') för lösenord eller att lagra känslig data okrypterad i miljövariabler utan ordentlig nyckelrotation.
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const SALT_ROUNDS = 12; // Tar ~250ms, tillräckligt för att försvåra brute force
// Hasha lösenord med bcrypt
const hashPassword = async (password) => {
return await bcrypt.hash(password, SALT_ROUNDS);
};
// Verifiera lösenord (constant-time jämförelse ingår i bcrypt)
const verifyPassword = async (password, hash) => {
return await bcrypt.compare(password, hash);
};
// Generera kryptografiskt säkert token (för lösenordsåterställning, etc.)
const generateSecureToken = (bytes = 32) => {
return crypto.randomBytes(bytes).toString('hex');
};
// Kryptera känslig data med AES-256-GCM
const encryptSensitiveData = (plaintext, key) => {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key, 'hex'), iv);
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString('hex'),
encrypted: encrypted.toString('hex'),
authTag: authTag.toString('hex'),
};
};
module.exports = { hashPassword, verifyPassword, generateSecureToken, encryptSensitiveData };
Bcrypt med 12 salt-rundor tar ungefär 250 millisekunder per hash på modern hårdvara. Det gör brute force-attacker praktiskt omöjliga: en angripare som testar 1 miljon lösenord per sekund (möjligt med GPU:er mot osaltad MD5) klarar bara 4 per sekund mot bcrypt med 12 rundor. AES-256-GCM är det rekommenderade läget för symmetrisk kryptering: det ger konfidentialitet (kryptering) och integritet (authenticated tag) i ett svep, och varje krypteringsoperation ska använda ett unikt, slumpmässigt IV.
Steg 5: Rate limiting och brute force-skydd (A07)
Identification and Authentication Failures (A07:2025) inkluderar svaga lösenordspolicyer, saknade skydd mot brute force, osäker “glömt lösenord”-funktionalitet och exponerade sessions-ID:n. Rate limiting är den enklaste och mest effektiva motåtgärden mot automatiserade inloggningsförsök och credential stuffing-attacker.
const rateLimit = require('express-rate-limit');
// Generell rate limit för alla endpoints
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minuter
max: 100, // max 100 anrop per fönster
standardHeaders: 'draft-7',
legacyHeaders: false,
message: { error: 'För många anrop, försök igen om 15 minuter' },
});
// Strikt limit för autentiseringsendpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // max 5 inloggningsförsök per 15 minuter
skipSuccessfulRequests: true,
standardHeaders: 'draft-7',
legacyHeaders: false,
message: { error: 'Kontot tillfälligt låst, försök igen om 15 minuter' },
});
// Limit för lösenordsåterställning
const passwordResetLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 timme
max: 3,
message: { error: 'Max 3 lösenordsåterställningar per timme' },
});
module.exports = { generalLimiter, authLimiter, passwordResetLimiter };
Credential stuffing-attacker använder läckta lösenordsdatabaser (2025 rapporterades 1,8 miljarder stulna inloggningsuppgifter) och prövar dem mot din applikation. Med en rate limit på 5 försök per 15 minuter tar det en angripare över 3 år att testa en lista med 10 000 lösenord. Utan rate limiting kan en angripare med botnet prova 100 000 kombinationer per minut.
Steg 6: JWT-säkerhet och sessions-hantering
Osäker JWT-implementering är ett av de vanligaste autentiseringsfelen i Node.js-applikationer. Typiska misstag inkluderar algoritmen none som godtas, för långa token-livslängar, och hemliga nycklar hårdkodade i källkoden. Skapa services/tokenService.js:
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET måste vara minst 32 tecken lång');
}
const ACCESS_TOKEN_EXPIRY = '15m'; // kort livslängd
const REFRESH_TOKEN_EXPIRY = '7d';
const generateTokens = (userId, role) => {
const accessToken = jwt.sign(
{ id: userId, role, type: 'access' },
process.env.JWT_SECRET,
{
expiresIn: ACCESS_TOKEN_EXPIRY,
algorithm: 'HS256',
issuer: 'myapp',
audience: 'myapp-users',
}
);
const refreshToken = jwt.sign(
{ id: userId, type: 'refresh', jti: crypto.randomUUID() },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY, algorithm: 'HS256' }
);
return { accessToken, refreshToken };
};
const verifyToken = (token, secret) => {
return jwt.verify(token, secret, {
algorithms: ['HS256'], // tillåt aldrig 'none'
issuer: 'myapp',
audience: 'myapp-users',
});
};
module.exports = { generateTokens, verifyToken };
Access tokens sätts till 15 minuters livslängd, vilket begränsar exponeringstiden om ett token komprometteras. Refresh tokens med 7 dagars livslängd lagras säkert i HTTP-only cookies (inte i localStorage som är åtkomligt via JavaScript och XSS-attacker). Algoritmen specificeras explicit som ['HS256'] i algorithms-arrayen, vilket förhindrar “alg: none”-attacker där en angripare skapar ett token utan signatur.
Steg 7: Beroendehantering och supply chain-säkerhet (A03 och A06)
Software Supply Chain Failures är den nyaste kategorin i OWASP Top 10:2025 och den som 50 procent av säkerhetsforskare rankar som det allvarligaste framtidshotet. Kategorin täcker komprometterade byggmiljöer, skadliga npm-paket och osäkra CI/CD-pipelines. CVE-2025-6514 i paketet mcp-remote möjliggjorde Remote Code Execution via OS-kommandoinjektion i OAuth-handskaken.
Skapa en automatiserad säkerhetscheck i package.json:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"security:audit": "npm audit --audit-level=high",
"security:scan": "npx snyk test",
"security:fix": "npm audit fix",
"prestart": "npm audit --audit-level=critical"
},
"engines": {
"node": ">=22.0.0"
}
}
Lägg till en .npmrc-fil för att blockera osäkra installations-scenarios:
# .npmrc
audit=true
fund=false
ignore-scripts=false
save-exact=true # pinnar exakta versioner, förhindrar oväntade uppgraderingar
package-lock=true
Med save-exact=true sparas exakt version i package.json (t.ex. "helmet": "8.0.0") i stället för "^8.0.0". Det förhindrar att en komprometterad patchversion (som vid Polyfill.io-attacken 2024 som drabbade 100 000 webbplatser) installeras automatiskt. Lägg alltid package-lock.json i versionskontroll och verifiera integriteten med npm ci i CI/CD i stället för npm install.
Steg 8: Säker loggning (A09)
Security Logging and Monitoring Failures (A09:2025) är ett underskattat problem. Utan tillräcklig loggning kan ett intrång pågå i månader utan att upptäckas, som vid de flesta stora dataläckor. Felet går också åt det andra hållet: loggar som innehåller lösenord, tokens eller personuppgifter i klartext skapar en ny sårbarhet.
Skapa en säker logger i utils/logger.js:
const winston = require('winston');
// Maskera känslig data i loggar
const maskSensitiveData = (obj) => {
if (!obj || typeof obj !== 'object') return obj;
const sensitive = ['password', 'token', 'secret', 'authorization', 'cookie', 'creditCard'];
const masked = { ...obj };
for (const key of Object.keys(masked)) {
if (sensitive.some(s => key.toLowerCase().includes(s))) {
masked[key] = '[MASKERAD]';
} else if (typeof masked[key] === 'object') {
masked[key] = maskSensitiveData(masked[key]);
}
}
return masked;
};
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
),
transports: [
new winston.transports.File({ filename: 'logs/security.log', level: 'warn' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ format: winston.format.simple() }));
}
// Logga säkerhetshändelse
const logSecurityEvent = (event, req, extra = {}) => {
logger.warn({
event,
ip: req.ip,
userId: req.user?.id || 'anonym',
userAgent: req.headers['user-agent'],
path: req.path,
method: req.method,
...maskSensitiveData(extra),
});
};
module.exports = { logger, logSecurityEvent };
Steg 9: SSRF-skydd (A10)
Server-Side Request Forgery (SSRF) är en kategori på stark uppgång och landade på tiondeplats i OWASP Top 10:2025. En SSRF-sårbarhet uppstår när applikationen gör HTTP-anrop till URL:er kontrollerade av användaren, vilket kan ge en angripare tillgång till interna tjänster som metadata-tjänsten på AWS (169.254.169.254) eller interna databaser.
const dns = require('dns').promises;
const net = require('net');
// Lista blockerade privata IP-intervall
const BLOCKED_RANGES = [
/^127\./, // localhost
/^10\./, // privat nät
/^172\.(1[6-9]|2\d|3[01])\./, // privat nät
/^192\.168\./, // privat nät
/^169\.254\./, // link-local (AWS metadata!)
/^::1$/, // IPv6 localhost
/^fc00:/, // IPv6 privat
/^fe80:/, // IPv6 link-local
];
const isPrivateIP = (ip) => BLOCKED_RANGES.some(re => re.test(ip));
// Validera URL innan externt anrop
const validateExternalUrl = async (urlString) => {
let url;
try {
url = new URL(urlString);
} catch {
throw new Error('Ogiltig URL');
}
// Tillåt bara HTTPS
if (url.protocol !== 'https:') {
throw new Error('Endast HTTPS tillåts');
}
// Lös upp hostname och kontrollera IP
const addresses = await dns.lookup(url.hostname, { all: true });
for (const { address } of addresses) {
if (isPrivateIP(address)) {
throw new Error('Åtkomst till interna adresser är förbjuden');
}
}
return url;
};
module.exports = { validateExternalUrl };
AWS EC2-metadata-tjänsten på 169.254.169.254 är det klassiska SSRF-målet och kan ge en angripare tillgång till IAM-credentials med full AWS-kontobehörighet. Valideringen löser upp DNS-namnet och kontrollerar att den faktiska IP-adressen inte är i ett privat intervall, vilket förhindrar DNS-rebinding-attacker där ett publikt domännamn pekar på en privat IP.
Steg 10: Integritetsskydd (A08)
Software and Data Integrity Failures (A08:2025) inkluderar osäkra deserialisering, CI/CD-kompromisser och autoUpgrade utan integritetskontroll. I Node.js är det vanligaste felet att använda eval(), new Function() eller JSON.parse() på opålitlig indata utan validering.
const crypto = require('crypto');
// Generera HMAC-signatur för data (t.ex. webhooks)
const signData = (data, secret) => {
const payload = typeof data === 'object' ? JSON.stringify(data) : String(data);
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
};
// Verifiera webhook-signatur (constant-time jämförelse)
const verifyWebhookSignature = (payload, signature, secret) => {
const expected = signData(payload, secret);
const expectedBuf = Buffer.from(expected, 'hex');
const receivedBuf = Buffer.from(signature, 'hex');
if (expectedBuf.length !== receivedBuf.length) return false;
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
};
// Säker JSON-parsning utan prototype pollution
const safeJsonParse = (str) => {
const parsed = JSON.parse(str);
if (parsed !== null && typeof parsed === 'object') {
// Förhindra prototype pollution
if ('__proto__' in parsed || 'constructor' in parsed) {
throw new Error('Misstänkt JSON-struktur');
}
}
return parsed;
};
module.exports = { signData, verifyWebhookSignature, safeJsonParse };
crypto.timingSafeEqual() är avgörande för signaturjämförelser. En vanlig strängkomparation i JavaScript (a === b) avbryts så snart den hittar ett tecken som skiljer sig, vilket gör det möjligt att mäta svarstiden och på så vis gissa fram signaturen bit för bit (timing attack). Den tidssäkra versionen tar alltid lika lång tid oavsett var skillnaden finns.
Steg 11: CORS och Cookie-säkerhet
Felkonfigurerad CORS är ett av de vanligaste säkerhetsfelen som faller under A02: Security Misconfiguration. Standardinställningen origin: '*' tillåter varje webbplats att göra autentiserade anrop till din API, vilket kan kombineras med XSS för att stjäla data. Cookies utan rätt flaggor är läsbara via JavaScript och kan föras vidare via HTTP.
const cors = require('cors');
const cookieParser = require('cookie-parser');
// CORS: tillåt bara definierade ursprung
const corsOptions = {
origin: (origin, callback) => {
const allowed = (process.env.ALLOWED_ORIGINS || '').split(',');
if (!origin || allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS blockerat för ursprung: ${origin}`));
}
},
credentials: true, // tillåt cookies med cross-origin-anrop
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // cache preflight 24 timmar
};
app.use(cors(corsOptions));
app.use(cookieParser(process.env.COOKIE_SECRET));
// Säker cookie för refresh token
const setRefreshTokenCookie = (res, token) => {
res.cookie('refreshToken', token, {
httpOnly: true, // ej åtkomlig via JavaScript
secure: true, // bara över HTTPS
sameSite: 'strict', // blockerar CSRF
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dagar
signed: true, // HMAC-signerad med COOKIE_SECRET
path: '/api/auth', // begränsad sökväg
});
};
Steg 12: Komplett applikation och felhantering
Samla allt i en komplett server.js med säker felhantering. Felmeddelanden mot klienten ska aldrig exponera stack traces, databasfel eller interna tekniska detaljer som en angripare kan använda för rekognosering.
require('dotenv').config();
const app = require('./app');
const { generalLimiter } = require('./middleware/rateLimiter');
const { logger } = require('./utils/logger');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const { sanitizeInput } = require('./validators/userValidator');
const PORT = process.env.PORT || 3000;
// Global sanitering
app.use(sanitizeInput);
// Rate limiting på alla routes
app.use('/api/', generalLimiter);
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// 404-hantering (avslöja inte teknisk info)
app.use((req, res) => {
res.status(404).json({ error: 'Resursen hittades inte' });
});
// Global felhantering
app.use((err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
path: req.path,
method: req.method,
ip: req.ip,
});
// Skicka aldrig intern felinformation till klienten
if (process.env.NODE_ENV === 'production') {
res.status(err.status || 500).json({ error: 'Internt serverfel' });
} else {
res.status(err.status || 500).json({
error: err.message,
stack: err.stack,
});
}
});
app.listen(PORT, () => {
logger.info(`Server startad på port ${PORT} i ${process.env.NODE_ENV}-läge`);
});
Skapa filen .env med alla hemliga värden. Lägg till .env i din .gitignore direkt:
# .env (lägg till .env i .gitignore!)
NODE_ENV=development
PORT=3000
JWT_SECRET=minst-32-tecken-langt-slumpmassigt-hemligt-varde
JWT_REFRESH_SECRET=ett-annat-minst-32-tecken-langt-varde
COOKIE_SECRET=cookie-signing-hemligt-varde-minst-32-tecken
ALLOWED_ORIGINS=http://localhost:3001,https://din-app.se
LOG_LEVEL=info
Verifiering: köra applikationen
Starta utvecklingsservern och verifiera att säkerhetsheaders är på plats:
npm run dev
# I ett annat terminalfönster, kontrollera headers:
curl -I http://localhost:3000/api/auth/login
# Förväntad output:
HTTP/1.1 404 Not Found
Content-Security-Policy: default-src 'self';...
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Verifiera att rate limiting fungerar:
# Prova 6 inloggningsförsök (ska blockeras vid det 6:e)
for i in {1..6}; do
curl -s -o /dev/null -w "Försök $i: %{http_code}\n" \
-X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"fel"}'
done
# Förväntad output:
Försök 1: 401
Försök 2: 401
Försök 3: 401
Försök 4: 401
Försök 5: 401
Försök 6: 429 (Too Many Requests)
5 vanliga fallgropar och hur du undviker dem
Dessa misstag dyker upp gång på gång i säkerhetsgranskningar av Node.js-applikationer:
Fallgrop 1: Hårdkodade hemligheter. Att lägga JWT-nycklar eller databas-lösenord direkt i källkoden är det vanligaste kritiska misstaget. Använd alltid miljövariabler och ladda dem med dotenv. Kör git log --all --full-history -- "**/.env" för att kontrollera att inga .env-filer läckt in i git-historiken.
Fallgrop 2: Felaktig Content-Security-Policy. Att sätta helmet() utan konfiguration ger en standardpolicy som kan vara för restriktiv (bryta befintliga funktioner) eller för tillåtlig. Testa CSP med report-only-läget innan du enforcar det: Content-Security-Policy-Report-Only.
Fallgrop 3: Glömd sökvägstraversering. Att servera filer med express.static() eller fs.readFile() baserat på användarinput utan validering öppnar för path traversal-attacker (../../../etc/passwd). Använd alltid path.resolve() och kontrollera att den lösta sökvägen börjar med din tilltänkta baskatalog.
Fallgrop 4: RegEx Denial of Service (ReDoS). Komplexa reguljära uttryck mot user-kontrollerad indata kan ta exponentiell tid att exekvera (catastrophic backtracking) och krascha servern. Undvik kapslade kvantifierare ((a+)+) och använd paketet safe-regex för att validera dina reguljära uttryck.
Fallgrop 5: Express body-parser utan storleksgräns. Utan limit: '10kb' i express.json() kan en angripare skicka ett JSON-objekt på 100 MB och krascha servern via minnesöverskott. Sätt alltid en explicit gräns.
Fallgrop 6: Felaktig HTTP-metod-hantering. Express svarar på alla HTTP-metoder om ingen begränsning sätts. En angripare kan skicka TRACE-anrop för att se server-intern information eller OPTIONS-anrop för att mappa vilka endpoints som finns. Blockera oönskade metoder explicit.
Fallgrop 7: Exponerade felmeddelanden i produktion. Stack traces i JSON-felresponser avslöjar teknisk infrastruktur (Node-version, biblioteksnamn, filsökvägar) som en angripare använder för att hitta kända CVE:er. Skilj alltid på development- och production-felhantering.
Fallgrop 8: Okrypterade sessionsdata i cookies. Att lagra känslig data direkt i cookies utan kryptering eller signering gör att en användare kan modifiera sina egna sessionsdata (inklusive roller). Använd alltid signed: true med COOKIE_SECRET och lagra bara session-ID i cookies, inte faktisk sessionsdata.
Felsökning: 8 vanliga problem
| Problem | Symptom | Lösning |
|---|---|---|
| helmet() bryter CSS/JS | Webbplats laddas men stilar saknas | Lägg till källa i CSP: styleSrc: ["'self'", "din-cdn.se"] |
| CORS-fel på frontend | Access to fetch blocked by CORS policy | Lägg till frontend-URL i ALLOWED_ORIGINS i .env |
| Rate limit blockerar API-anrop | 429 Too Many Requests i test | Öka max i testmiljön, eller lägg till IP i whitelist |
| JWT-verifiering misslyckas | JsonWebTokenError: invalid signature | Kontrollera att JWT_SECRET är identisk i sign och verify |
| bcrypt är för långsamt | Inloggning tar 3+ sekunder | Minska SALT_ROUNDS till 10 i dev. I prod är 12 rätt. |
| mongo-sanitize tar bort legitimt data | Fältnamn med punkt försvinner | Tillåt punkter selektivt: allowDots: true i sanitize-config |
| Cookie ej satt i browser | refreshToken syns inte i DevTools | Kontrollera att secure: true kräver HTTPS; använd HTTPS i test |
| NODE_ENV ej satt i produktion | Stack traces syns i API-svar | Sätt NODE_ENV=production i produktionsmiljöns miljövariabler |
Avancerade tips för produktionsmiljö
Subresource Integrity (SRI) för externa resurser. Om du laddar CSS eller JS från ett CDN, lägg till SRI-hash-attributet i dina script- och link-taggar. Det garanterar att webbläsaren bara kör koden om den matchar den förväntade hashen, vilket skyddar mot komprometterade CDN-leverantörer.
Automatisk sårbarhetsskanning i CI/CD. Lägg till npm audit --audit-level=high som ett steg i din GitHub Actions- eller GitLab CI-pipeline. Om kommandot returnerar ett non-zero exit-code (dvs. hög eller kritisk sårbarhet hittas) avbryts deployen automatiskt. Det förhindrar att känd sårbar kod når produktion.
Rotera krypteringsnycklar regelbundet. JWT-hemligheter och krypteringsnycklar bör roteras med en definierad frekvens (exempelvis kvartalsvis). Implementera nyckelversionshantering med ett kid-fält i JWT-headern så att tokens signerade med den gamla nyckeln kan valideras under övergångsperioden.
Aktivera Node.js permission model. I Node.js 22+ kan du starta servern med --experimental-permission --allow-fs-read=. --allow-net för att sandboxa applikationen på OS-nivå. Var medveten om att tre av de åtta CVE:erna som patchades i januari 2026 (CVE-2025-55132, CVE-2026-21636, CVE-2025-55130) var just bypasses av permission-systemet via symlänkar och Unix Domain Sockets.
Aktivera audit-loggning för känsliga operationer. Alla ändringar av behörigheter, lösenordsbyten, och åtkomst till känslig data bör loggas med tidsstämpel, användar-ID, IP-adress och åtgärd. Det är inte bara god praxis utan ett krav under NIS2-direktivet som i Sverige trädde i kraft 15 januari 2026 via Cybersäkerhetslagen.
Implementera Security Headers-test. Kör securityheaders.com mot din staging-miljö och sikta på betyget A+. Testet verifierar att alla headers är korrekt konfigurerade och ger konkreta förbättringsförslag.
Komplett projektöversikt
Ditt färdiga projekt ska ha följande struktur:
owasp-nodejs-demo/
├── .env (ej i git)
├── .env.example (mall utan värden, i git)
├── .gitignore
├── .npmrc
├── package.json
├── server.js (startpunkt)
├── app.js (Express-konfiguration)
├── middleware/
│ ├── auth.js (authenticate, authorize, checkResourceOwnership)
│ └── rateLimiter.js (generalLimiter, authLimiter)
├── routes/
│ ├── auth.js (login, register, refresh, logout)
│ └── users.js (CRUD med åtkomstkontroll)
├── services/
│ ├── tokenService.js (generateTokens, verifyToken)
│ └── passwordService.js (hashPassword, verifyPassword)
├── validators/
│ └── userValidator.js (registerValidation, sanitizeInput)
├── utils/
│ ├── logger.js (winston logger med maskning)
│ └── ssrfGuard.js (validateExternalUrl)
└── logs/ (genereras vid körning, ej i git)
Kör en sista säkerhetsrevision innan deployment:
# Kontrollera sårbara beroenden
npm audit --audit-level=high
# Sök efter hårdkodade hemligheter
grep -r "password\|secret\|api_key\|token" --include="*.js" . | grep -v node_modules | grep -v ".env" | grep -v logger
# Kontrollera att .env inte är i git
git ls-files .env
# Kontrollera att NODE_ENV är satt
echo $NODE_ENV
Vanliga frågor om OWASP Top 10 i Node.js
Vad är skillnaden mellan OWASP Top 10:2025 och 2021? De tre största förändringarna är: Security Misconfiguration klättrade från femte till andraplats, Software Supply Chain Failures (A03) är en helt ny kategori som reflekterar hotet från skadliga npm-paket och komprometterade CI/CD-pipelines, och Injection sjönk från förstaplats till femteplats trots att det fortfarande finns i varje testad applikation.
Måste jag implementera alla tio skydden? Ja, om din applikation hanterar användardata, autentisering eller externa anrop. OWASP-listan är inte en prioriteringslista utan en checklista. En applikation som bara saknar skydd mot A10 (SSRF) men är säker på övriga nio kan ändå komprometteras fullständigt om en SSRF-sårbarhet finns.
Är helmet() tillräckligt för Security Misconfiguration? Helmet täcker HTTP-headers, vilket är en viktig del av A02. Men Security Misconfiguration inkluderar också: exponerade felmeddelanden (konfigureras i felhanteraren), öppna directory listings (blockeras med express.static()-konfiguration), onödiga aktiverade features och osäkra standardvärden i databaser och molntjänster.
Vad är NoSQL-injektion och hur skiljer det sig från SQL-injektion? SQL-injektion manipulerar SQL-frågor med specialtecken. NoSQL-injektion mot MongoDB manipulerar i stället JSON-operatörer. Payload {"password": {"$gt": ""}} i inloggningsformuläret matchar alla lösenord som är “greater than” en tom sträng, vilket ger angriparen tillgång till kontot utan rätt lösenord. Express-mongo-sanitize löser detta automatiskt.
Hur ofta bör jag köra säkerhetsrevisioner? Kör npm audit som del av din CI/CD-pipeline vid varje commit. Kör en mer grundlig revision med Snyk eller OWASP ZAP kvartalsvis eller inför varje större release. Under NIS2/Cybersäkerhetslagen (som gäller ca 8 000 svenska företag) är regelbundna riskbedömningar ett lagkrav.
Vilka Node.js-versioner är säkra att använda 2026? Node.js 22.x LTS och Node.js 20.x LTS (underhållet till april 2026) är de rekommenderade versionerna. Node.js 17.x och 19.x nådde end-of-life och har kritiska oslagade CVE:er (CVE-2025-23087, CVE-2025-23088) som ger angripare möjlighet till remote code execution.
Kan jag använda dessa skydd med Fastify istället för Express? Ja, principerna gäller för alla Node.js-ramverk. Fastify har inbyggda motsvarigheter till många Express-middleware: @fastify/helmet, @fastify/rate-limit och @fastify/jwt. Valideringslogiken i express-validator ersätts av Fastify’s inbyggda JSON Schema-validering.
Vad är skillnaden mellan autentisering och auktorisering? Autentisering verifierar vem du är (JWT-token, lösenord). Auktorisering avgör vad du får göra (rolkontroller, resursägandekontroll). Broken Access Control (A01:2025) uppstår nästan alltid när applikationer autentiserar korrekt men missar auktoriseringen, till exempel när en autentiserad användare kan läsa en annan användares data.
Relaterat innehåll
Fördjupa dig vidare i angränsande ämnen:
- Digitala signaturer i Node.js med ECDSA: 12 steg [2026]
- OpenSSL 3.5: nycklar och certifikat i 12 steg [2026]
- Cybersäkerhetslagen: 8 000 företag, 2 % böter [2026]
- PGP-kryptering med GPG: e-post i 12 steg [2026]
- Ransomware 2026: Sverige värst i Norden, 60 incidenter [2026]
- Säkerhet: fullständig guide till cybersäkerhet [2026]
Externa resurser:



