---
title: "Pentest: solid app, men origin-serveren omgikk hele Cloudflare"
date: 2026-06-03
description: "Oppdraget ba om RCE og masseuttrekk av brukerdata. Appen var for godt bygget — men origin-serveren lå åpent på nett og omgikk hele Cloudflare-laget."
tags: ["pentest", "security", "cloudflare", "nextjs"]
url: https://erik.no/blogg/2026-06-03-solid-app-origin-omgar-cloudflare
---

<Callout type="info">
  **FIKTIVT:** Alle firmanavn, personnavn, domener og e‑postadresser i dette
  innlegget er oppdiktet. Eventuell likhet med ekte virksomheter eller
  personer er tilfeldig. Detaljer fra reelle oppdrag er sanert.
</Callout>

Jeg ble leid inn for å sikkerhetsteste en læringsplattform for kliniske kasus —
fortsatt i beta — hos Fjordlys AS (`fjordlysas.no`). Oppdragsbeskrivelsen var
tydelig på målet: få ut brukerdata i bulk, og helst remote code execution. Det
korte svaret er at ingen av delene lot seg gjøre. Det interessante svaret er
*hvorfor*, og hva jeg fant i stedet.

## Kontekst

Stacken var moderne og ryddig: Cloudflare foran en Caddy-server, som proxer en
Next.js-app (App Router), autentisering via Clerk i middleware, dataflyt gjennom
React Server Actions, Prisma mot PostgreSQL. Jeg fikk tre testkontoer fra kunden:
en registrert-men-ikke-godkjent bruker, et vanlig medlem, og et andre medlem til
å teste tilgangskontroll bruker mot bruker.

Selve testingen kjørte jeg som et moderne oppsett med AI i loopen. AI-en skrev et
lite Python-harness som logget inn via Clerk, holdt bearer-tokenet ferskt, og
fanget server-action-kallene via Chrome DevTools Protocol — slik at jeg kunne
spille dem av igjen med egne argumenter. Jeg styrte retningen; AI-en gjorde
mye av tastingen.

## Det jeg ikke fant — og hvorfor det også teller

Mesteparten av tiden gikk til de tunge målene i SOW-en, og de holdt ikke vann:

- **SQL-injection:** ingen. Prisma parameteriserer alt. `' OR '1'='1` ga 500 / null
  rader, og `SLEEP(5)` ga 0,08 sekunder — ingen forsinkelse.
- **RCE og auth-bypass:** ingen. Hele CVE-2026-klyngen for Next.js, "React2Shell"
  og CVE-2025-29927 var patchet.
- **XSS:** ingen. React HTML-escaper all input, både reflektert og lagret.
- **IDOR bruker mot bruker:** ingen. Med det andre medlemmet bekreftet jeg at både
  lesing og skriving på fremmede caser ble nektet server-side.

For å være sikker på at det ikke bare var Cloudflare som reddet appen, testet jeg
også **direkte mot origin** (mer om det under) — uten WAF foran. Rammeverket var
fortsatt patchet. Det er en viktig nyanse: et verktøy som rapporterer "blokkert"
forteller deg bare at edge gjorde jobben sin, ikke at koden er trygg.

<Callout type="info">
  Et rent resultat er også en leveranse. "Jeg forsøkte RCE og masseuttrekk, her er
  nøyaktig hva som ble testet, og det holdt" er verdt like mye i en rapport som en
  liste med funn — så lenge du kan vise *hva* du faktisk prøvde.
</Callout>

Det er også her menneskelig dømmekraft må inn. En av de automatiske CVE-sjekkene
ropte "VULNERABLE" på en CSP-nonce-XSS. Jeg verifiserte for hånd: nonce-verdien
var HTML-escapet (`&quot;`), ingen reell breakout. Falsk positiv. Verktøyet
avgjør ikke den slags — det må en lese selv.

## Det jeg faktisk fant

De reelle funnene lå ikke i applikasjonskoden, men i infrastruktur og konfigurasjon.

### Origin-serveren omgikk hele Cloudflare — <Severity level="high" />

Headline-funnet. Origin-serveren (`203.0.113.10`) svarte direkte på det åpne
internett på `22/tcp` (SSH), `80/tcp` og `443/tcp` (Caddy). Den var ikke
IP-begrenset til Cloudflare, og Authenticated Origin Pulls var ikke slått på.

Origin-en var ikke *synlig* utenfra — jeg lot AI-en kjøre en bred origin-jakt
(crt.sh, passiv DNS, CloudFail, CloakQuest3r mot 5000 subdomener, Censys), og alt
pekte tilbake på Cloudflare. Caddy krever riktig SNI, så skannere klarte aldri å
koble IP-en til hostnavnet. Men da kunden selv oppga IP-en, var resten triviell:

```bash
curl --resolve fjordlysas.no:443:203.0.113.10 https://fjordlysas.no/
# HTTP/2 200 · via: 1.1 Caddy · x-powered-by: Next.js
# buildId "aB3xK9mQ2pLw7vRn"  → eksakt match med produksjon
# ingen cf-ray-header        → svaret kom fra origin, ikke fra Cloudflare
```

Ingen `cf-ray`-header betyr at forespørselen aldri var innom Cloudflare. Dermed er
*alle* edge-kontroller — WAF, rate limiting, bot-håndtering, DDoS-beskyttelse —
satt ut av spill for trafikk som sendes rett på origin. Rammeverket er patchet i
dag, så det er ingen umiddelbar vei til kjøring av kode. Men neste sårbarhet, i
appen eller i Next.js, ville hatt null beskyttelse foran seg. En WAF kan ikke
beskytte det som kan omgås.

<Callout type="warning">
  Lås origin-en til Cloudflares IP-ranges i brannmuren, slå på Authenticated
  Origin Pulls (mTLS), og begrens SSH til en bastion eller allowlist. En WAF som
  kan omgås er pynt.
</Callout>

### E-postdomenet kunne forfalskes — <Severity level="medium" />

`fjordlysas.no` hadde **ingen DMARC-policy** og en SPF som endte på `~all`
(softfail). Jeg ba AI-en dumpe DNS-en, og hullet var åpenbart:

```text
_dmarc.fjordlysas.no   → NXDOMAIN                        (ingen DMARC)
TXT fjordlysas.no      → v=spf1 include:icloud.com ~all   (softfail, ikke -all)
```

Så sendte jeg en melding med både envelope-from og header-from satt til
`kontakt@fjordlysas.no`, fra en IP uten noen relasjon til domenet. Den ble
akseptert — både av iCloud og av Microsoft 365:

```text
MAIL FROM:<kontakt@fjordlysas.no>   (fra 198.51.100.23, ikke en iCloud-IP)
RCPT TO:<kontakt@fjordlysas.no>     → 250 2.1.5 Ok
DATA                                → 250 2.0.0 Ok: queued as <REDACTED>
```

Meldingsteksten var en tydelig merket, ikke-villedende test-notis. Poenget var
ikke å lure noen, men å vise at hvem som helst kan sende e-post som ser ut til å
komme fra `@fjordlysas.no` — gefundenes fressen for phishing av studenter,
ansatte og partnere. Fiksen er kjedelig og kjent: publiser DMARC (`p=none` med
rapportering, så `p=reject`), og stram SPF til `-all`.

### For mye data i case-API-et — <Severity level="low" />

Server-actionen som henter en enkelt case returnerte mer enn UI-et trenger:
hele case-objektet *pluss* forfatterens brukerrecord (`clerkId`, fornavn,
etternavn) og *all* tilbakemelding andre brukere hadde lagt igjen.

```json
{
  "case": { "...": "..." },
  "user": { "clerkId": "<REDACTED>", "firstName": "Mari", "lastName": "Haugen" },
  "organization": { "id": "...", "name": "..." },
  "caseFeedbacks": [ { "message": "..." } ]
}
```

Et hvilket som helst medlem som åpnet en (offentlig) case fikk dermed forfatterens
fulle navn og Clerk-ID, og kunne lese tredjeparts tilbakemeldinger. Ikke en
exploit, men en datamininerings-/personvern-sak. Fiksen er å returnere en minimal
DTO fra lese-modellen.

### Klient-side DoS på delte caser — <Severity level="low" />

Når man åpnet en case med manglende data og gikk til "Evaluering"-steget, kræsjet
render-bunten:

```text
TypeError: Cannot read properties of null (reading 'general')
→ React error boundary → "Application error: a client-side exception has occurred"
```

Siden caser kan være **offentlige** (og dukker opp via en "tilfeldig case"-funksjon),
betyr en case lagret med null-felter at *alle* som når det steget får en helsides
kræsj. En lagret, klient-side DoS mot delt innhold. En null-guard på rendringen
løser det.

I tillegg lå det en håndfull lav-alvorlige ting: manglende sikkerhetsheadere
(HSTS, CSP, X-Frame-Options), TLS 1.0/1.1 tilbudt på edge, og 500 i stedet for
401/403 på autentiseringsgrensen.

## Arbeidsmåten: AI i loopen, menneske ved roret

Dette var i stor grad meg som operatør og prompter, og det vil jeg være ærlig om.
AI-en skrev harnesset, kjørte origin-sweepen, genererte og kjørte sqlmap- og
nuclei-pass, og parset logger. Det meste av sqlmap-trafikken ble forresten spist
av WAF-en (168 × 403), så verktøyet testet egentlig Cloudflare og ikke appen —
de manuelle clean-request-testene var de som faktisk talte. Der måtte jeg lese
resultatene selv.

De delene som krever et menneske, gjorde jeg:

- **Scope og etikk.** Et par treff under origin-jakten viste seg å være helt
  urelaterte servere. De hørte ikke hjemme i oppdraget, så de ble luket ut og
  artefaktene slettet. DoS-PoC-ene i CVE-suiten kjørte jeg ikke — de stod
  eksplisitt utenfor scope.
- **Triage av funn.** Den falske CSP-nonce-positiven over er det beste eksemplet.
  Verktøyet sa "sårbar"; jeg sa "escapet, ergo ikke".
- **Prioritering og konklusjoner.** AI-en krediteres ikke for at Cloudflare-bypass
  ble High og manglende headere ble Low. Den vurderingen, og den endelige
  rapporten, er min.

Tonen jeg sikter mot er moderne pentest med AI som kraftmultiplikator — ikke
"se så manuell jeg er", og heller ikke "se så flink AI-en er".

## Hva jeg lærte

- **En velbygd app flytter risikoen til infrastrukturen.** Når koden holder, er
  det origin-eksponering, e-postkonfig og datamininering som blir de reelle
  funnene.
- **Test alltid direkte mot origin når du kan.** Ellers vet du ikke om det er
  appen eller WAF-en som stopper deg.
- **Automatiserte verktøy er et førsteutkast, ikke en konklusjon.** WAF-403 og
  falske positiver gjør at noen må lese resultatene med hodet.
- **Et rent resultat er en leveranse** — så lenge du dokumenterer nøyaktig hva som
  ble forsøkt.

## Veien videre

Oppdraget inkluderer én retest etter at Fjordlys AS har rettet. Det jeg ville
gjort annerledes neste gang: bedt om origin-IP-en tidligere, slik at jeg kunne
kjørt hele CVE-batteriet direkte mot origin fra start i stedet for å bruke tid på
å bekrefte at edge blokkerte ting. Den ene tilgangen avgjorde halve testen.
