Ordinær eksamen - Kodeanalyse og sikkerhet
Type: Kodeanalyse-eksamen med Docker Compose-system
Varighet: 3-4 timer (anbefalt)
Format: Skriv egne svar, deretter sammenlign med sensorveiledning
Fokus: Systemforståelse, kodeanalyse, sikkerhetsvurdering
Eksamenen tar utgangspunkt i et komplett system bestående av følgende filer (klikk for å se innhold):
Beskrivelse: Shell-script som bygger og starter systemet
#!/bin/sh podman-compose up --build
Beskrivelse: Definerer de to containerne og deres konfigurasjon
services:
web:
build: web
ports:
- "8080:80"
db:
build: db
ports:
- "8000:8000"
Beskrivelse: Bygger en nginx-basert webserver for frontend
FROM docker.io/library/nginx:alpine COPY index.html /usr/share/nginx/html/
Beskrivelse: JavaScript-basert grensesnitt som kommuniserer med REST API
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Personer</title>
</head>
<body>
<h1>Personer</h1>
<div id="personer"></div>
<script>
// JavaScript for å hente og vise personer
fetch('http://localhost:8000/personer')
.then(r => r.json())
.then(data => {
document.getElementById('personer').innerHTML =
data.map(p => `<p>${p.navn}</p>`).join('');
});
</script>
</body>
</html>
Beskrivelse: Bygger container med C-webtjener og shell-basert REST API
FROM docker.io/library/alpine:latest RUN apk add --no-cache gcc musl-dev sqlite COPY db.c db.sh / RUN gcc -o db db.c EXPOSE 8000 CMD ["/db"]
Beskrivelse: Shell-script som håndterer REST API-forespørsler
#!/bin/sh
# REST API håndtering
METHOD=$REQUEST_METHOD
PATH=$REQUEST_URI
case $METHOD in
GET)
sqlite3 personer.db "SELECT * FROM personer;"
;;
POST)
# Håndter POST-forespørsel
;;
esac
Beskrivelse: C-program som lytter på port 8000 og starter db.sh for hver forespørsel
🎓 Pedagogisk tilnærming: Klikk på hver kodelinje for å se forklaring. Prøv først å analysere selv!
📚 Forklaring: Inkluderer standard input/output bibliotek for funksjoner som printf()
📚 Forklaring: Inkluderer socket-bibliotek som gir tilgang til socket(), bind(), listen(), accept()
📚 Forklaring: Definerer strukturer for internett-adresser (sockaddr_in, INADDR_ANY, htons)
📚 Forklaring: POSIX API for fork(), dup2(), close(), execl()
📚 Forklaring: Oppretter en TCP socket (SOCK_STREAM) for IPv4 (AF_INET)
🔍 Nøkkelkonsept: AF_INET = IPv4, SOCK_STREAM = TCP, returnerer file descriptor
📚 Forklaring: Definerer adresse-struktur:
• sin_family = AF_INET → IPv4
• sin_addr.s_addr = INADDR_ANY → Lytt på alle IP-adresser (0.0.0.0)
• sin_port = htons(8000) → Port 8000 (htons = host to network short - konverterer til big-endian)
📚 Forklaring: Binder socketen til adressen (IP + port)
⚠️ Viktig: Denne linjen "reserverer" port 8000 slik at socket kan motta trafikk der
📚 Forklaring: Setter socketen i "lyttemodus" med kø-lengde på 10
🔍 Dette betyr: Maks 10 ventende klient-tilkoblinger før nye blir avvist
📚 Forklaring: Evig løkke - serveren kjører kontinuerlig
📚 Forklaring: Blokkerende kall som venter på klient-tilkobling
🔍 Viktig: Returnerer ny file descriptor for klient-socket når tilkobling mottas
📚 Forklaring: Oppretter ny prosess (child). fork() returnerer 0 i barneprosessen
⭐ Hvorfor? Parallell håndtering - hver klient får sin egen prosess
📚 Forklaring: Omdirigerer STDIN (0) til klient-socket
🔍 Effekt: db.sh kan lese fra klienten via standard input
📚 Forklaring: Omdirigerer STDOUT (1) til klient-socket
🔍 Effekt: db.sh kan skrive til klienten via standard output
📚 Forklaring: Erstatter barneprosessen med db.sh
⚠️ Viktig: execl() returnerer ALDRI ved suksess - prosessen blir db.sh
🔍 Resultat: db.sh arver omdirigerte file descriptors (STDIN/STDOUT → socket)
📚 Forklaring: Slutt på if-blokk (barneprosess)
📚 Forklaring: Foreldreprosessen lukker sin kopi av klient-socket
⭐ Hvorfor? Foreldreprosessen trenger ikke denne - kun barneprosessen bruker den
📚 Forklaring: Slutt på while-løkke - går tilbake til accept()
Spørsmål 1: Hva er formålet med fork() i denne koden?
🤔 Tenk på: Hva skjer når flere klienter kobler til samtidig? Kan serveren håndtere dem parallelt?
fork() oppretter en ny prosess (child) for hver klient-tilkobling. Dette gjør at serveren kan håndtere flere klienter samtidig (concurrent handling). Mens barneprosessen kjører db.sh for én klient, kan foreldreprosessen gå tilbake til accept() og ta imot nye tilkoblinger.
Uten fork(): Serveren måtte vente til én klient var ferdig før den kunne ta imot neste.
Spørsmål 2: Hvorfor brukes dup2() to ganger? Hva oppnås?
🤔 Tenk på: Hva er STDIN (0) og STDOUT (1)? Hvor leser/skriver db.sh?
dup2() omdirigerer file descriptors:
• dup2(client, 0) → STDIN blir klient-socket (db.sh kan lese HTTP-forespørsel)
• dup2(client, 1) → STDOUT blir klient-socket (db.sh kan skrive HTTP-respons)
Resultat: db.sh tror den leser fra tastatur og skriver til terminal, men kommuniserer faktisk med klienten over nettverket!
Spørsmål 3: Hvorfor lukker foreldreprosessen client-socketen med close(client)?
🤔 Tenk på: fork() kopierer alle file descriptors. Hvem trenger client-socketen?
Etter fork() har både forelder og barn en kopi av client file descriptor. Foreldreprosessen trenger ikke denne - den skal bare vente på nye tilkoblinger med accept().
Viktig: Hvis foreldreprosessen ikke lukker sin kopi, vil socketen forbli åpen selv når barneprosessen er ferdig, noe som kan føre til ressurslekkasje (file descriptor exhaustion).
Gi en overordnet beskrivelse av systemet.
• Identifiser hovedkomponentene (containere, tjenester)
• Beskriv hvordan de samarbeider
• Forklar systemets overordnede formål
Dette er et system som består av to containere:
Systemets funksjon: Brukere kan via nettleseren bruke databasen gjennom et JavaScript-basert grensesnitt. Grensesnittet sender HTTP-forespørsler til REST-APIet som håndterer CRUD-operasjoner (Create, Read, Update, Delete) mot databasen.
Arkitektur: Typisk mikrotjeneste-arkitektur hvor frontend og backend er separert i egne containere med definerte kommunikasjonsporter.
Forklar hvilken rolle/hensikt hver av filene i systemet har. Begrunn hvordan du kommer frem til svarene.
Oppgaven er todelt: (1) Forklaring og (2) Begrunnelse
Disse vektes likt (5% + 5%). Du må forklare BÅDE hva filen gjør OG hvorfor du konkluderer med det.
s.sh
Rolle: Systemutviklingsverktøy som stopper systemet, (om)bygger det og starter det opp igjen.
Begrunnelse: Kommandoene `podman-compose down`, `build` og `up` er en typisk utviklingssyklus. Dette er et verktøy for utviklere, ikke en del av det ferdige systemet.
compose.yaml
Rolle: Orkestreringsfile for styring av de to containerne.
Begrunnelse: Filen følger Docker Compose-standarden og definerer services, ports og build-kontekst for begge containere. Den gjør det enkelt å administrere flerkontainer-applikasjonen.
web/Containerfile
Rolle: Oppskrift på bygging av container-bildet for frontend-containeren.
Begrunnelse: Inneholder Dockerfile-instruksjoner (FROM, COPY, EXPOSE, CMD) som definerer hvordan web-containeren skal bygges. Bruker busybox og starter httpd-webserver.
web/index.html
Rolle: Brukergrensesnitt som leveres til nettleseren.
Begrunnelse: HTML-fil med JavaScript som henter data fra input-felt og sender fetch()-requests til REST-APIet. Fungerer som SPA (Single Page Application) for databaseinteraksjon.
db/Containerfile
Rolle: Oppskrift på bygging av container-bildet for backend-containeren med REST-API og database.
Begrunnelse: Installerer nødvendige pakker (sqlite, jq, uuidgen), oppretter database-skjema, setter opp testdata og kopierer inn API-koden.
db/db.sh
Rolle: Implementerer REST-APIet som håndterer HTTP-forespørsler og databaseoperasjoner.
Begrunnelse: Shell-script som leser HTTP-requests fra stdin, parser dem, utfører SQL-queries og returnerer JSON-responses. Håndterer autentisering, sesjoner og CRUD-operasjoner.
db/db.c
Rolle: Enkel webtjener som aksepterer HTTP-tilkoblinger og delegerer til db.sh-prosesser.
Begrunnelse: C-program som lytter på port 80, aksepterer tilkoblinger, forker nye prosesser og bruker dup2() til å koble socket til stdin/stdout før den execer db.sh. Klassisk pre-fork server-pattern.
Gi en detaljert beskrivelse av koden i filene.
Viktig: Selv om det ikke er nødvendig med en forklaring linje-for-linje, bør alle deler av koden være forklart.
• Vis forståelse for sammenhengen koden kjøres i
• Når det leses fra stdin → forklar at det er klientforespørselen
• Når det skrives til stdout → forklar at det havner i HTTP-responsen
• Beskriv effekten av utskrifter hos klienten (f.eks. HTTP-headere)
Se eksamen-oppgavesettet for fullstendige kodelistinger. Her er en rask oversikt:
Generelt krav: Forklaringen skal vise forståelse for:
Nøkkelpunkter som må dekkes:
db.c (Web Server):
db.sh (REST API):
index.html (Frontend):
Containerfiler:
⚠️ Viktig: Forklaringen må vise at du forstår at stdin/stdout i db.sh er koblet til socket-tilkoblingen, slik at det som leses er HTTP-requesten fra klienten og det som skrives blir HTTP-responsen tilbake.
Gi en vurdering av autentiseringsmekanismen som brukes i systemet.
Foreslå en forbedring. Begrunn svaret.
• Hvordan fungerer autentiseringen? (login + cookie-basert sesjon)
• Hva er bra med løsningen?
• Hvilke sikkerhetsproblemer eksisterer?
• Foreslå konkrete forbedringer
Det som er bra:
Sikkerhetsproblemer:
Forslag til forbedringer:
Set-Cookie: SESJONSID=...; HttpOnly; Secure; SameSite=Strict
💡 Viktigste forbedring: HTTPS er kritisk! Uten kryptering er all autentisering verdiløs på usikre nettverk.
Hvordan ville du endret systemet slik at det kun var mulig for autentiserte brukere å endre eget passord?
La svaret ditt være mest mulig konkret og detaljert.
• Oppdateringsmetoden i REST-APIet (linjer 101-103 i db/db.sh) inkluderer innsetting av ny passordhash
• JavaScript-funksjonen oppdater() (linjer 29-36 i web/index.html) inkluderer IKKE passordet
• Systemet har allerede autentisering og sesjoner på plass
Det finnes mange mulige løsninger. Viktig at svaret omtaler endringer i BÅDE web/index.html OG db/db.sh.
Løsningsforslag 1: Separat endrePassord-funksjon
I web/index.html:
// Legg til nytt input-felt:
<input type='password' id='nyttPassord' placeholder='nytt passord'>
<button onclick='endrePassord()'>Endre passord</button>
// Ny JavaScript-funksjon:
function endrePassord() {
brukerid = document.querySelector('#brukerid').value;
gammeltPw = document.querySelector('#passord').value;
nyttPw = document.querySelector('#nyttPassord').value;
fetch(new URL('/endrePw/'+brukerid, base), {
method: 'put',
body: JSON.stringify({
gammeltPassord: gammeltPw,
nyttPassord: nyttPw
}),
credentials: 'include',
headers: {'Content-Type': 'application/json'}
});
}
I db/db.sh:
// Legg til ny endpoint (etter linje 66):
elif [ "$TAB" = "endrePw" ] && [ "$REQUEST_METHOD" = "PUT" ]; then
# Hent gammelt og nytt passord fra JSON
GAMMELT_PW=$(echo "$KR" | jq -r '.gammeltPassord')
NYTT_PW=$(echo "$KR" | jq -r '.nyttPassord')
# Verifiser gammelt passord
LAGRET_HASH=$(echo "SELECT passordhash FROM person WHERE brukerid='$RAD'" | sqlite3 /person.db)
GAMMELT_PW_HASH=$(echo $GAMMELT_PW | mkpasswd -S $SALT -)
if [ "$GAMMELT_PW_HASH" != "$LAGRET_HASH" ]; then
printf "\r\n"
echo '[{"feilmelding":"Feil gammelt passord"}]'
exit
fi
# Generer ny hash med nytt salt
NY_SALT=$(head -c16 /dev/urandom | base64 | tr -d '=+/')
NY_HASH=$(echo $NYTT_PW | mkpasswd -S $NY_SALT -)
# Oppdater i database
echo "UPDATE person SET passordhash='$NY_HASH' WHERE brukerid='$RAD'" | sqlite3 /person.db
printf "\r\n"
echo '[{"melding":"Passord oppdatert"}]'
exit
fi
Løsningsforslag 2: Utvid eksisterende oppdater()-funksjon
Alternativt kan oppdater()-funksjonen utvides til å også håndtere passordendring hvis et nytt passord er oppgitt. Dette krever:
Sikkerhetshensyn:
Velg en fil fra listen over for å vise koden...