Det viser seg gang, på gang, på gang, at open source kan kompromitteres. Dette betyr at vi via open source kan installere ondsinnet kode. Dessverre ser dette ut til å være en økende trend. Ondsinnede aktører, ikke bare personer, men stater og organisasjoner, prøver å hacke oss via ondsinnet open source-kode.

I det store bildet skjer dette relativt sjeldent, men det har blitt en betydelig angrepsvektor, så det er på høy tid å beskytte oss.

Denne typen angrep kalles gjerne “Supply chain attack”. Dette er egentlig et ganske vidt begrep, men vi fokuserer her på angrep mot programvarebiblioteker.

Du kan ikke lenger stole på applikasjonen din

Vi er avhengige av open source for å lage vår egen software. For eksempel krever en grunnleggende React Vite-app (npm create vite@latest) cirka 56 MB med npm-pakker, fordelt på 102 implisitte og 11 eksplisitte avhengigheter. Har du tid til å sjekke at alt dette er uskyldig kode? Neppe.

Når du laster ned kode og eksekverer den, kjører den med alle rettigheter din bruker har. Om du ikke har tatt visse forhåndsregler, kjører dette med din hovedbruker, som har mange rettigheter i systemet. Det betyr f.eks. at koden kan søke gjennom filsystemet ditt, finne interessante filer og sende dem ut på internett, sannsynligvis uten at du merker noe som helst.

Om slik kode blir med ut i prod, kan en bare tenke seg hva slags ugagn den kan finne på…

De fleste sikkerhetsløsninger, som vi stoler på for å løse dette, handler om at slik kode blir oppdaget av mennesker, programmer, AI-er o.l., for så å bli markert som skadelig. Dette kan ta tid, og om du er som meg; vil bare oppgradere til “latest and greatest”, så er det stor sjanse for at ondsinnet kode kommer inn på systemet ditt før det blir oppdaget. Det er heller ikke utenkelig at det forblir skjult.

Enda verre er det at de fleste plattformer virker som om de nærmest ignorerer problemet. Ut fra min research, av de store plattformene, det er kun Deno og Wasm som prøver å løse problemet ordentlig.

Deno

Deno ble skapt av Ryan Dahl som et alternativ til Node. Og da Deno 2 ble lansert, ble det i praksis en drop-in replacement for Node, med ganske store forbedringer. Men viktigst for meg, det tar det supply chain attacks på alvor.

Ved oppstart av en Deno-app må du angi hva appen får lov til å gjøre. Dette er f.eks. hvilke deler av filsystemet den kan lese og skrive til. En må også angi hvilke domener den kan snakke med. Får en inn ondsinnet kode, så må bruker eksplisitt akseptere tilgang, ellers vil koden bare feile med en exception.

Deno sin metode krever opt-in, du må altså angi hva appen får lov til, som er mye sikrere enn opt-out, der du angir hva den ikke får lov til. En kan enkelt skru det helt av også, men dette er selvsagt ikke anbefalt.

Eksempel (kildekode):

> deno index.js
┏ ⚠️  Deno requests net access to "0.0.0.0:3000".
┠─ Requested by `Deno.listen()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-net
┠─ Run again with --allow-net to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) > y

✅ Granted net access to "0.0.0.0:3000".
Server running at http://localhost:3000/

// I et annet shell:
> curl localhost:3000

// I dette shellet:
┏ ⚠️  Deno requests read access to "/Users/stoyle/code/repos/permissions/local-file.txt".
┠─ Requested by `Deno.readFileSync()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-read
┠─ Run again with --allow-read to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) > y

✅ Granted read access to "/Users/stoyle/code/repos/permissions/local-file.txt".
┏ ⚠️  Deno requests net access to "example.com:443".
┠─ Requested by `node:dns.lookup()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-net
┠─ Run again with --allow-net to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) > y
✅ Granted read access to "/Users/stoyle/code/repos/permissions/local-file.txt".
✅ Granted net access to "example.com:443".

Dersom jeg mot formodning ikke gir tilgang til example.com skjer dette:

❌ Denied net access to "example.com:443".
error: Uncaught (in promise) Error: getaddrinfo EPERM example.com
    at __node_internal_captureLargerStackTrace (ext:deno_node/internal/errors.ts:91:3)
    at __node_internal_ (ext:deno_node/internal/errors.ts:244:10)
    at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:42:26)
    at ext:deno_node/internal_binding/cares_wrap.ts:82:9
    at eventLoopTick (ext:core/01_core.js:178:7)

Deno har altså en interaktiv sesjon der du ser hva appen krever av tilganger. Så kan en legge disse til i oppstart av appen. Eksempel:

> deno run --allow-read=./local-file.txt --allow-net=example.com,0.0.0.0:3000 index.js
Server running at http://localhost:3000/

I prod ville en typisk slått av interaktivitet, dette kan gjøres med --no-prompt. Si at jeg da hadde ondsinnet kode som forsøkte å lese filen local-file.txt i prod uten at jeg hadde tilgang, da ville dette skjedd:

> deno run --no-prompt --allow-net=example.com,0.0.0.0:3000 index.js
Server running at http://localhost:3000/

error: Uncaught (in promise) NotCapable: Requires read access to "./local-file.txt", run again with the --allow-read flag
      readFileSync: (path) => new TextDecoder().decode(Deno.readFileSync(path))
                                                            ^
    at Object.readFileSync (ext:deno_fs/30_fs.js:740:10)
    at Object.readFileSync (file:///Users/stoyle/code/repos/permissions/index.js:16:61)
    at ServerImpl.<anonymous> (file:///Users/stoyle/code/repos/permissions/index.js:28:28)
    at ServerImpl.emit (ext:deno_node/_events.mjs:394:28)
    at node:http:1549:18
    at new Promise (<anonymous>)
    at handler (node:http:1532:16)
    at mapped (ext:deno_http/00_serve.ts:399:24)
    at mapped (ext:deno_http/00_serve.ts:495:16)
    at ext:deno_http/00_serve.ts:752:29

Altså, i verste fall så vil appen gå ned, evt. får en masse exceptions i logger, men ellers har ingen skade skjedd.

I Deno (og Node) kan kode kjøre som en del av install (pre- og postinstall scripts). Deno “has your back”:

> deno add npm:msw
Add npm:msw@2.10.2
Warning The following packages contained npm lifecycle scripts (preinstall/install/postinstall) that were not executed:
┠─ npm:msw@2.10.2
┃
┠─ This may cause the packages to not work correctly.
┖─ To run lifecycle scripts, use the `--allow-scripts` flag with `deno install`:
   deno install --allow-scripts=npm:msw@2.10.2

I Deno gjelder tilgangene globalt for applikasjonen. Den skiller ikke på din kode eller biblioteker. Dette er en enkel modell å forholde seg til, men Wasm har tatt steget lenger.

Wasm

WebAssembly (Wasm) er annerledes. Det er designet for å kjøre i nettleser, der sandboxing er veldig viktig for at ikke ondsinnet kode skal kunne slippe løs på systemet ditt. Wasm kan også kjøre server side.

I motsetning til Deno, så sandboxer Wasm hver individuelle modul. Altså kan en spesifisere tilganger pr modul, ikke pr applikasjon. Kort forklart, når du bootstrapper applikasjonen kan denne koden eksplisitt gi tilgang til individuelle moduler, om det er dine egne eller for tredjepartsbiblioteker.

Dette gir enda finere granularitet enn det Deno har. F.eks. kan du la din kode få skrivetilgang til filsystemet, mens biblioteker ikke har tilgang i det hele tatt.

Les gjerne mer om Wasm i bloggposten Hva er WebAssembly?

Hva så med Node?

Node versjon 23.5.0 (utgitt 19. desember 2024), slapp sitt eget tilgangssystem. Jeg vil tro, i stor grad inspirert av Deno. Dessverre ikke i nærheten så kraftig eller brukervennlig. I tillegg må en eksplisitt velge å slå det på, derfor tror jeg svært få bruker det. I tillegg:

  • Ingen interaktivitet - alt må slås på i oppstart, ellers krasjer app
  • Ingen --allow-net - altså ingen måte å begrense domener appen kan kalle

Jeg tenker noe av det viktigste en kan gjøre er å begrense domener applikasjonen kan kalle på, dette er altså ikke mulig i node. Men for å starte appen med permissions, så kan jeg f.eks. gjøre det sånn her.

node --permission --allow-fs-read=. index.js

Jeg mener dette ikke er godt nok, og det føles litt som om Node bare har “sjekket av” dette problemet, uten å forsøke å løse det ordentlig. Om du bruker Node, gjør deg selv en tjeneste og test ut Deno, sannsynligvis enklere å komme i gang med enn du tror.

Hva med resten, Java, Python, .NET osv.

Dette er et ganske alvorlig problem som jeg ikke kan se noen andre har tatt aktive steg mot å bekjempe. F.eks. hadde både Java og .NET sikkerhetsmodeller, men disse har blitt fjernet. Det er godt mulig dette var legacy som ikke beskyttet mot dagens trusler, men de har heller ikke kommet med noen alternativer.

Det er synd, jeg oppfatter at disse plattformene kan være minst like attraktive å angripe. Dette er tross alt “backend kode”, mens Deno, Node og Wasm kanskje assosieres mer med frontend.

Hva bør du gjøre nå?

Vi har tidligere tenkt at truslene hovedsaklig kommer utenfra. Men, vi må nok i større grad innse at vi også må beskytte oss mot ondsinnet kode fra avhengigheter, altså innenfra. Skal du starte et nytt prosjekt, evt. om du bruker Node, vurder Deno, eventuelt Wasm. Det er dessverre ikke så enkelt for alle.

Det finnes en del grep du kan gjøre uansett. F.eks. om du deployer appen din via docker, sørg for at applikasjonen ikke kjører med rot-tilgang. Kjører du i Kubernetes finnes det løsninger som begrenser utgående trafikk. Lokalt bruker jeg Little Snitch sånn at jeg eksplisitt må gi tilgang til apper og hvilke domener de vil kontakte, og for all del, sørg for å ha backups.

Gjør litt research på hvordan du kan beskytte deg best mulig. Ut ifra det jeg har lest meg fram til, de færreste løsningene kan oppnå sikkerheten som kreves. Men nå er du klar over problemet, på tide å diskutere dette og ta grep.