Den dag min lille hobby-side blev et sikkerheds-mareridt
Jeg troede helt ærligt ikke, at nogen gad angribe en lille hobby-side med 20 brugere og en halvdårlig logo-skitse. Det tog cirka én eftermiddag at få det modsatte bevist.
Hvis du også sidder og bygger små ting på nettet og tænker “det er jo bare et lille projekt”, så er teksten her til dig.
Den første gang jeg så mærkeligt input i databasen
Det startede med en simpel kommentar-funktion. Brugere kunne skrive en besked, og jeg gemte den i databasen og viste den på siden.
// før - min naive PHP-kode
$comment = $_POST['comment'];
$query = "INSERT INTO comments (text) VALUES ('$comment')";
mysqli_query($conn, $query);
En dag dukkede der noget nyt op i databasen:
<script>alert('hej')</script>
Det så uskyldigt ud, indtil jeg åbnede siden og fik en alert-boks lige i ansigtet. Det var min uofficielle introduktion til XSS.
Mini-trusselsmodel for små projekter
Hvis du tænker “hvem gider dog angribe mig?”, så prøv at vende den om: du behøver ikke en målrettet hacker. Du skal bare bruge én person med lidt for meget tid, eller et script der scanner tilfældige sider.
Typiske ting der kan gå galt i små projekter:
- Brugere kan køre JavaScript i andres browser (XSS) og stjæle sessions.
- Nogen kan slette eller læse alle rækker i din database (SQL injection).
- Password-databasen bliver lækket, og dine brugere genbruger samme password andre steder.
- Din API bliver misbrugt, fordi du tror CORS er en sikkerhedsbarriere.
- Et gammelt dependency har en kendt sårbarhed, som bots scanner efter.
Det lyder dramatisk, men pointen er: du behøver ikke forstå alt om sikkerhed. Du skal bare undgå de mest klassiske huller. Resten kan du lære stille og roligt hen ad vejen.
Input-validering og output-escaping: XSS forklaret med et simpelt eksempel
Typisk begynder-fejl: du tager tekst fra en formular og skriver den direkte ud i HTML.
Hvad er XSS i praksis?
XSS (Cross-Site Scripting) betyder, at en bruger får lov til at smide sit eget JavaScript ind i din side. Hvis det sker, kan de f.eks.:
- Stjæle cookies eller session-tokens.
- Få andre brugere til at sende requests uden at vide det.
- Manipulere hvad der står på siden.
Det er ikke “bare” en popup. Popuppen er bare demo-versionen.
Før: direkte output
// Node/Express eksempel
app.post('/comment', (req, res) => {
const comment = req.body.comment;
comments.push(comment);
res.redirect('/');
});
app.get('/', (req, res) => {
res.send(`
<ul>
${comments.map(c => `<li>${c}</li>`).join('')}
</ul>
`);
});
Hvis en bruger skriver <script>alert(1)</script>, bliver det kørt i browseren.
Efter: escape output
Løsningen er ikke bare “valider input”. Du skal især escape output, når du skriver tekst ind i HTML.
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
app.get('/', (req, res) => {
res.send(`
<ul>
${comments.map(c => `<li>${escapeHtml(c)}</li>`).join('')}
</ul>
`);
});
Nu bliver <script> vist som tekst i stedet for kørt.
Hvordan tester du at det virker?
- Indtast i en formular:
<script>alert('xss')</script>. - Genindlæs siden.
- Hvis du får en popup, er der et problem.
- Hvis du bare ser teksten, er du på rette vej.
Hvis du vil nørde videre, er OWASP Cheat Sheet om XSS et godt sted at kigge. Du kan også finde flere introduktioner på dansk via Coding Class.
SQL injection: den dag min SELECT blev til DROP TABLE
Anden klassiker: du laver en søgefunktion eller login, og sætter input direkte ind i en SQL-streng.
Før: string-konkatenering
// før - klassisk PHP-stil
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
Hvis en bruger skriver:
' OR '1'='1
i feltet for brugernavn, kan det blive til:
SELECT * FROM users
WHERE username = '' OR '1'='1'
AND password = 'whatever';
Så får de måske logget ind uden at kende et password.
Efter: prepared statements
Ideen bag prepared statements er ret simpel: du sender SQL og data hver for sig. Databasen ved, hvad der er struktur, og hvad der er indhold.
// Node + mysql2
const [rows] = await conn.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
Her bliver username og password sendt som parametre. De kan ikke ændre strukturen i din SQL, kun værdierne.
Hvordan tester du?
- Prøv at logge ind med brugernavn:
' OR '1'='1og et tilfældigt password. - Hvis du bliver logget ind, har du et problem.
- Hvis du får “forkert login”, ser det bedre ud.
Det er en meget simpel test, men den fanger faktisk en del begynderskader.
Auth basics: sessions vs JWT som begynder
Der er mange holdninger til auth. Jeg er selv landet på en stille og rolig konklusion: hvis du bygger en klassisk web-app, så brug sessions og cookies. JWT kan du tage senere.
Sessions: den stille klassiker
Sessions fungerer typisk sådan her:
- Bruger logger ind.
- Serveren opretter en session i databasen eller i memory (et id med lidt data).
- Serveren sender et cookie med et session-id til brugeren.
- Ved hver request sender browseren cookie med, og serveren slår sessionen op.
Som begynder er fordelene:
- Let at forstå: der er et session-id, og du slår det op.
- Let at invalidere: slet sessionen, og brugeren er logget ud.
JWT: mere fleksibelt, mere at holde styr på
JWT (JSON Web Tokens) er smarte til API’er, microservices og mobile apps. Men der er flere faldgruber: udløbstider, revokering, opbevaring i browseren, osv.
Jeg vil faktisk anbefale: hvis du er helt ny, så start med sessions og HttpOnly-cookies. JWT kan du tage, når du har styr på de grundlæggende mønstre.
Hvis du vil læse op på sessions, cookies og login-flow, så har vi en intro til HTTP og tilstand på Coding Class.
Passwords: hashing og hvorfor “kryptering” ikke er nok
En af mine første fejl var at gemme passwords i klartekst i databasen. Det var før nogen heldigvis gjorde mig opmærksom på, at det var en rigtig dårlig idé.
Hvad sker der hvis databasen lækkes?
Hvis du gemmer passwords i klartekst, og din database bliver kopieret, så har angriberen alles logins. Og de fleste genbruger passwords. Så skaden stopper ikke ved din lille app.
Hashing vs kryptering
- Kryptering: du kan gå frem og tilbage (kryptere og dekryptere) med en nøgle.
- Hashing: du kan kun gå én vej. Du kan ikke genskabe password fra hash.
Til passwords bruger man hashing med en langsom algoritme, f.eks. bcrypt, Argon2 eller PBKDF2.
Før: klartekst (gør det ikke)
// før - sådan her startede jeg
INSERT INTO users (username, password) VALUES ('mikkel', 'hemligt123');
Efter: hashing med bcrypt
// Node + bcryptjs
import bcrypt from 'bcryptjs';
// når du opretter bruger
const hashed = await bcrypt.hash(password, 12);
await conn.execute('INSERT INTO users (username, password_hash) VALUES (?, ?)', [username, hashed]);
// når du logger ind
const [rows] = await conn.execute('SELECT * FROM users WHERE username = ?', [username]);
const user = rows[0];
const ok = await bcrypt.compare(password, user.password_hash);
if (!ok) {
// forkert login
}
Hvordan tester du?
- Kig i databasen: står der noget der ligner det rigtige password, er det galt.
- Står der en lang tilfældig streng (ofte med
$2b$i starten for bcrypt), er du på vej i den rigtige retning.
HTTPS og cookies: små flag med stor effekt
Jeg kørte selv HTTP lidt for længe, fordi det virkede “technisk besværligt” at få HTTPS til at spille. I dag er der faktisk ingen god undskyldning. Let’s Encrypt gør det gratis, og de fleste hosts hjælper med opsætningen.
Hvorfor HTTPS?
Uden HTTPS kan alle på nettet mellem brugeren og din server læse trafikken. På offentlige wifi er det ekstra slemt. Med HTTPS bliver trafikken krypteret.
Det betyder bl.a.:
- Passwords bliver ikke sendt i klartekst.
- Session-cookies kan ikke bare opsnappes med et simpelt sniffer-værktøj.
Cookie-flags: Secure, HttpOnly og SameSite
Når du bruger cookies til auth, skal du sætte et par små flags:
- Secure: cookie sendes kun over HTTPS.
- HttpOnly: JavaScript kan ikke læse cookie (beskytter mod XSS-angreb der vil stjæle cookies).
- SameSite: styrer om cookies sendes med cross-site requests (kan hjælpe mod CSRF).
Eksempel i Express
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true, // kræver HTTPS
sameSite: 'lax' // fornuftig default
});
Hvis du er nysgerrig på security headers generelt, så er MDN et godt opslagsværk, og du kan eksperimentere med ting som Content-Security-Policy, når du er klar til næste niveau.
CORS forklaret: hvad det er, og hvad det ikke er
Jeg har brugt overraskende mange aftener på at stirre på CORS-fejl i browserens konsol. Og jeg har googlet “disable CORS” mere end jeg har lyst til at indrømme.
Hvad er CORS egentlig?
CORS (Cross-Origin Resource Sharing) handler primært om, hvad browseren tillader JavaScript at gøre. Det er en sikkerhedsmekanisme i browseren, ikke en firewall for din server.
Hvis du får en fejl ala “No ‘Access-Control-Allow-Origin’ header is present”, så betyder det, at din frontend på f.eks. http://localhost:3000 prøver at kalde en API på http://localhost:5000, og serveren siger ikke tydeligt “det er ok”.
Hvad CORS ikke er
- Det er ikke en måde at beskytte din API mod misbrug.
- Det er ikke en erstatning for auth.
- Det er ikke nok at “slå CORS fra” og tro alt er sikkert.
En simpel CORS-opsætning
// Node + express + cors
import cors from 'cors';
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
Til hobbyprojekter er det fint at starte med én origin (din frontend-URL), og så ikke åbne op for hele verden med *, med mindre du ved hvorfor.
Hvis du vil forstå CORS mere grundigt, er MDN’s artikel om CORS faktisk en af de mere fordøjelige tekniske tekster derude.
Dependencies: små pakker, store problemer
Jeg elsker at copy-paste fra Stack Overflow og installere en eller anden npm-pakke klokken 23:17. Men hver gang du installerer noget, tager du et lille sikkerheds-lån.
Risikoen ved dependencies
- Pakken kan have en kendt sårbarhed.
- Pakken kan blive overtaget og opdateret med ondsindet kode.
- Du kan importere mere end du har brug for, og åbne unødvendige angrebsflader.
Hvad jeg gør i dag
- Tjekker sidste opdateringsdato på GitHub/npm.
- Kigger kort i issues eller README for at se om projektet virker levende.
- Fjerner dependencies jeg faktisk ikke bruger længere.
Som minimum: kør npm audit en gang imellem, eller brug de sikkerheds-advarsler GitHub giver dig på repos. Det gør ikke alt sikkert, men du undgår de mest åbenlyse huller.
Sikkerheds-tjekliste: 10 ting jeg ville ønske jeg gjorde før første deploy
Her er min egen lille tjekliste, destilleret fra for mange aftener med fejlfinding:
1. Alle formularer: XSS-test
- Input på alle tekstfelter:
<script>alert('xss')</script>. - Genindlæs siden og se, om noget JavaScript kører.
- Hvis ja: escape output, især i HTML.
2. Alle database-queries: brug prepared statements
- Ingen string-konkatenering med user input i SQL.
- Skift til
? / $1-parametre og prepared statements. - Test med input:
' OR '1'='1og se om du slipper igennem.
3. Passwords: hash, ikke klartekst
- Installér bcrypt/Argon2 eller tilsvarende.
- Gem
password_hashi databasen, aldrig rent password. - Tjek databasen manuelt: du må ikke kunne genkende brugerens password.
4. Slå HTTPS til
- Brug Let’s Encrypt eller din hosts indbyggede certifikat-løsning.
- Redirect automatisk fra HTTP til HTTPS.
5. Sæt cookie-flags korrekt
- Auth-cookie med
HttpOnly,SecureogSameSite=LaxellerStrict. - Tjek i udviklerværktøjer i browseren, om flagene faktisk er sat.
6. Begræns hvad brugere må uploade
- Hvis du har filupload, tillad kun bestemte filtyper.
- Gem aldrig uploadede filer i en mappe, der bliver kørt som kode på serveren.
7. Vis ikke stack traces til brugeren
- I production: slå detaljerede fejlbeskeder fra.
- Log fejlen på serveren, men vis en neutral fejl-side til brugeren.
8. Tjek standard-login og admin-brugere
- Ingen accounts med password
admin,123456eller lignende. - Skift default-passwords i eventuelle admin-interfaces eller databaser.
9. CORS: vær specifik
- Åbn kun for de origins du faktisk bruger.
- Undgå
Access-Control-Allow-Origin: *sammen med credentials, med mindre du ved præcist hvorfor.
10. Dependencies: ryd op
- Fjern pakker du ikke bruger.
- Opdater de mest kritiske (framework, auth, ORM osv.).
- Kør de automatiske sikkerhedstjek, dit værktøj tilbyder.
Hvis du vil have mere struktur på dine projekter og sikkerhed, kan du med fordel kombinere tjeklisten her med noget versionsstyring. Vi har en intro til Git og versionsstyring som kan hjælpe med ikke at miste overblikket, når du retter fejl.
Hvor jeg selv står i dag
Jeg er stadig ikke sikkerhedsekspert. Jeg er mest bare ham, der fik et par knubs for tidligt og så begyndte at skrive tingene ned. Min oplevelse er, at 80 % af begynderskaderne kan undgås med relativt få vaner.
Hvis du kan sige ja til de fleste punkter i tjeklisten, er du allerede foran mange hobbyprojekter (og nogle produktionsprojekter, hvis vi skal være helt ærlige).
Og hvis du en dag også sidder med en uventet alert-boks og kold kaffe klokken 23:41, så kan du i det mindste trøste dig med, at du ikke er den første der har lavet den fejl. Jeg har som regel bare gjort den en uge tidligere.








Send kommentar
Du skal være logget ind for at skrive en kommentar.