Tilbake
7.2
Sikker programmering

7.2 Sikker programmering

OWASP Top 10, injeksjon, XSS og inputvalidering.

65 min
7 oppgaver
OWASPSQL-injeksjonXSSInputvalidering
Du leser den tradisjonelle versjonen
Din fremgang i kapitlet
0 / 7 oppgaver

Sikker programmering handler om å skrive kode som er resistent mot angrep. Mange sikkerhetshull oppstår fordi utviklere stoler for mye på brukerinput eller glemmer å implementere grunnleggende sikkerhetstiltak.

I dette kapittelet skal vi se på de vanligste sikkerhetssårbarhetene i webapplikasjoner og hvordan du som utvikler kan beskytte deg mot dem. Vi fokuserer spesielt på input-validering, SQL injection, XSS og CSRF.

Det viktigste prinsippet i sikker programmering er: Aldri stol på brukerinput.

All data som kommer fra brukere - enten det er fra skjemaer, URL-parametere, cookies eller filer - må valideres og saniteres før det brukes.

Typer validering:

1. Klientside-validering (JavaScript)
- Rask feedback til brukeren
- Kan enkelt omgås
- Aldri stol på denne alene!

2. Serverside-validering (Python/Node.js)
- Obligatorisk sikkerhetstiltak
- Kan ikke omgås av brukeren
- Siste forsvarslinje

3. Database-validering
- Ekstra lag med sikkerhet
- Constraints og foreign keys

Valideringsteknikker:

Whitelist (tillatte verdier):

# Kun tillat spesifikke verdier
gyldig_sort = ['navn', 'dato', 'pris']
if sortering in gyldig_sort:
    # Sikker å bruke
    sorter_etter(sortering)

Type-sjekk:

# Sjekk at input er riktig datatype
alder = request.form.get('alder')
try:
    alder = int(alder)
    if 0 < alder < 120:
        # Gyldig alder
        lagre_alder(alder)
except ValueError:
    return "Ugyldig alder"

Lengdebegrensning:

# Begrens lengde på input
navn = request.form.get('navn', '')
if len(navn) > 100:
    return "Navn for langt"

Regex-validering:

import re

# Valider e-postformat
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if re.match(email_pattern, email):
    # Gyldig e-post
    send_email(email)

SQL injection er en av de mest alvorlige sårbarhetene (A03 i OWASP Top 10). Den oppstår når angripere kan injisere SQL-kode gjennom brukerinput.

Usikker kode:

# ALDRI gjør dette!
brukernavn = request.form.get('brukernavn')
query = f"SELECT * FROM brukere WHERE brukernavn = '{brukernavn}'"
cursor.execute(query)

Angrep:
Hvis en angriper skriver inn: ' OR '1'='1
Blir spørringen: SELECT * FROM brukere WHERE brukernavn = '' OR '1'='1'
Dette returnerer alle brukere!

Enda verre: '; DROP TABLE brukere; --
Dette kan slette hele tabellen.

Sikker kode med parameteriserte spørringer:

# SQLite (Python)
brukernavn = request.form.get('brukernavn')
cursor.execute(
    "SELECT * FROM brukere WHERE brukernavn = ?",
    (brukernavn,)
)

# PostgreSQL med psycopg2
cursor.execute(
    "SELECT * FROM brukere WHERE brukernavn = %s",
    (brukernavn,)
)

Hvorfor dette er sikkert:
- Databasen skiller mellom SQL-kode og data
- Input behandles alltid som data, aldri som kode
- Spesialtegn escapes automatisk

ORM (Object-Relational Mapping):
Bruk av ORM som SQLAlchemy gjør det lettere å unngå SQL injection:

from sqlalchemy.orm import Session
from models import Bruker

# Sikkert med ORM
bruker = session.query(Bruker).filter(
    Bruker.brukernavn == brukernavn
).first()

XSS oppstår når en angriper kan injisere skadelig JavaScript i en webside som andre brukere ser.

Typer XSS:

1. Stored XSS (lagret)
Ondsinnet kode lagres i database og vises til alle brukere.
Eksempel: Kommentarfelt på blogg

2. Reflected XSS (reflektert)
Ondsinnet kode sendes som parameter og reflekteres tilbake til brukeren.
Eksempel: Søkeresultat-side

3. DOM-based XSS
Sårbarheten ligger i klientside JavaScript-kode.

Eksempel på angrep:

<!-- Usikker visning av brukerinput -->
<p>Velkommen, {{ brukernavn }}</p>

Hvis brukernavn er: <script>alert(document.cookie)</script>
Vil scriptet kjøres i nettleseren til alle som ser siden.

Angriper kan:
- Stjele cookies og sesjonstokens
- Omdirigere til phishing-sider
- Endre innhold på siden
- Kjøre keyloggers

Beskyttelse mot XSS:

1. Output encoding/escaping:

# Flask escaper automatisk i Jinja2-templates
# Men sørg for at autoescaping er på:
{{ brukernavn }}  # Sikkert

# Ikke bruk |safe uten grunn:
{{ bruker_html|safe }}  # Farlig!

HTML-escaping gjør:
- < blir til &lt;
- > blir til &gt;
- " blir til &quot;
- ' blir til &#x27;
- & blir til &amp;

2. Content Security Policy (CSP):

# Flask
@app.after_request
def set_csp(response):
    response.headers['Content-Security-Policy'] = \
        "default-src 'self'; script-src 'self'"
    return response

Dette hindrer nettleseren i å kjøre inline scripts og scripts fra andre domener.

3. Sanitering av HTML:

Hvis du MÅ tillate HTML-input (f.eks. rich text editor):

import bleach

# Tillat bare sikre tags
allowed_tags = ['p', 'b', 'i', 'u', 'a']
allowed_attrs = {'a': ['href', 'title']}

ren_html = bleach.clean(
    bruker_html,
    tags=allowed_tags,
    attributes=allowed_attrs,
    strip=True
)

CSRF (uttales "sea-surf") er et angrep der en ondsinnet side lurer nettleseren til å sende forespørsler til en annen side der brukeren er innlogget.

Eksempel på angrep:

Du er innlogget på bank.no. Du besøker ondsinnet.com som inneholder:

<img src="https://bank.no/overforing?til=angriper&belop=10000">

Nettleseren sender automatisk din innloggede sesjon med forespørselen!

Beskyttelse: CSRF-tokens

# Flask med Flask-WTF
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField

class OverforingForm(FlaskForm):
    til_konto = StringField('Til konto')
    belop = StringField('Beløp')
    submit = SubmitField('Overfør')

@app.route('/overforing', methods=['GET', 'POST'])
def overforing():
    form = OverforingForm()
    if form.validate_on_submit():
        # CSRF-token er validert
        # Trygt å utføre overføring
        utfor_overforing(form.til_konto.data, form.belop.data)
    return render_template('overforing.html', form=form)

I template:

<form method="POST">
    {{ form.csrf_token }}  <!-- Automatisk CSRF-token -->
    {{ form.til_konto.label }} {{ form.til_konto }}
    {{ form.belop.label }} {{ form.belop }}
    {{ form.submit }}
</form>

Hvordan CSRF-token fungerer:

1. Server genererer unikt, hemmelig token for hver sesjon
2. Token inkluderes i alle skjemaer som hidden field
3. Ved POST-forespørsel sjekker server at token matcher
4. Ondsinne sider kan ikke kjenne til tokenet

Andre CSRF-beskyttelser:

- SameSite cookies: Forhindrer sending av cookies på cross-site forespørsler
- Double Submit Cookie: Send token både som cookie og i form
- Custom headers: Krever X-Requested-With header for API-kall

Sjekkliste for sikker kode:

Input-håndtering:
- [ ] All brukerinput valideres på server-side
- [ ] Whitelist brukes der mulig
- [ ] Type og lengde sjekkes
- [ ] Feilmeldinger avslører ikke sensitiv info

Database:
- [ ] Parameteriserte spørringer brukes konsekvent
- [ ] Minste privilegium-prinsippet (database-bruker har kun nødvendige rettigheter)
- [ ] Sensitive data krypteres

Autentisering og autorisasjon:
- [ ] Passord hashes (aldri lagret i klartekst)
- [ ] Sesjoner har timeout
- [ ] Autorisasjon sjekkes for hver handling
- [ ] Brute force-beskyttelse implementert

Output:
- [ ] All output escapes/encodes
- [ ] CSP-header satt
- [ ] Ingen sensitive data i HTML-kommentarer eller JavaScript

CSRF-beskyttelse:
- [ ] CSRF-tokens på alle state-changing operasjoner
- [ ] SameSite cookies brukes

HTTPS og transport:
- [ ] All kommunikasjon over HTTPS
- [ ] Secure og HttpOnly flags på cookies
- [ ] HSTS-header aktivert

Dependencies:
- [ ] Alle avhengigheter er oppdaterte
- [ ] Ingen kjente sårbarheter (bruk npm audit eller pip-audit)

Logging og overvåkning:
- [ ] Sikkerhetshendelser logges
- [ ] Ingen passord eller tokens i logger
- [ ] Varsling ved mistenkelig aktivitet

✏️Eksempel: Sikker bloggkommentar-funksjon

La oss implementere en sikker funksjon for å legge til bloggkommentarer:

from flask import Flask, request, render_template, redirect, session
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
import sqlite3
import bleach
from datetime import datetime

app = Flask(__name__)
app.config['SECRET_KEY'] = 'din-hemmelige-nøkkel-her'

class KommentarForm(FlaskForm):
    innhold = TextAreaField(
        'Kommentar',
        validators=[
            DataRequired(message='Kommentar kan ikke være tom'),
            Length(min=1, max=1000, message='Kommentar må være mellom 1-1000 tegn')
        ]
    )
    submit = SubmitField('Legg til kommentar')

def get_db():
    conn = sqlite3.connect('blogg.db')
    conn.row_factory = sqlite3.Row
    return conn

@app.route('/innlegg/<int:innlegg_id>', methods=['GET', 'POST'])
def vis_innlegg(innlegg_id):
    # Sjekk at bruker er innlogget
    if 'bruker_id' not in session:
        return redirect('/login')

    form = KommentarForm()

    if form.validate_on_submit():
        # CSRF-token er allerede validert av Flask-WTF

        # Hent og valider input
        innhold = form.innhold.data

        # Sanitér HTML (tillat kun sikre tags)
        allowed_tags = ['p', 'b', 'i', 'u', 'br']
        ren_innhold = bleach.clean(
            innhold,
            tags=allowed_tags,
            strip=True
        )

        # Bruk parameterisert spørring (SQL injection-beskyttelse)
        db = get_db()
        try:
            db.execute(
                """
                INSERT INTO kommentarer (innlegg_id, bruker_id, innhold, opprettet)
                VALUES (?, ?, ?, ?)
                """,
                (innlegg_id, session['bruker_id'], ren_innhold, datetime.now())
            )
            db.commit()

            # Logg hendelsen (for overvåkning)
            app.logger.info(
                f"Kommentar lagt til av bruker {session['bruker_id']} "
                f"på innlegg {innlegg_id}"
            )

        except sqlite3.Error as e:
            # Ikke avslør database-detaljer til bruker
            app.logger.error(f"Database-feil: {e}")
            return "En feil oppstod. Prøv igjen senere.", 500
        finally:
            db.close()

        return redirect(f'/innlegg/{innlegg_id}')

    # Hent innlegg og kommentarer
    db = get_db()
    innlegg = db.execute(
        "SELECT * FROM innlegg WHERE id = ?",
        (innlegg_id,)
    ).fetchone()

    if not innlegg:
        return "Innlegg ikke funnet", 404

    kommentarer = db.execute(
        """
        SELECT k.*, b.brukernavn
        FROM kommentarer k
        JOIN brukere b ON k.bruker_id = b.id
        WHERE k.innlegg_id = ?
        ORDER BY k.opprettet DESC
        """,
        (innlegg_id,)
    ).fetchall()
    db.close()

    return render_template(
        'innlegg.html',
        innlegg=innlegg,
        kommentarer=kommentarer,
        form=form
    )

if __name__ == '__main__':
    # I produksjon: Ikke bruk debug=True, bruk HTTPS
    app.run(debug=False, ssl_context='adhoc')

Sikkerhetstiltak implementert:

1. CSRF-beskyttelse: Flask-WTF validerer automatisk token
2. Input-validering: WTForms validators sjekker lengde og innhold
3. XSS-beskyttelse: Bleach saniterer HTML
4. SQL injection-beskyttelse: Parameteriserte spørringer
5. Autentisering: Sjekker at bruker er innlogget
6. Error handling: Skjuler tekniske detaljer for bruker
7. Logging: Logger hendelser for overvåkning
8. HTTPS: SSL-context i produksjon

Oppsummering

I dette kapittelet har du lært:

- Sikker programmering: validere og sanere input.
- SQL injection: forhindres med parameteriserte spørringer.
- XSS: forhindres ved å escape brukerinput (autoescaping).
- Input-validering: sjekk type, lengde og format.
- CSRF-beskyttelse: f.eks. Flask-WTF.

Noekkelbegreper


BegrepForklaring
Input-valideringSjekke at brukerinput er gyldig
SQL injectionAngrep via uvalidert input i SQL
XSSInnsetting av skadelig skript i nettsider

Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.