Att köra kod som en språkmodell skrivit — utan att bli hackad

Användaren laddar upp ett kalkylblad och skriver vad som ska ändras med egna ord. En språkmodell skriver Python-kod. Koden körs mot filen.

Läs den sista meningen igen. Det är en av de mest riskabla konstruktioner man kan bygga, och den blir vanligare för varje månad — för den är också oerhört användbar.

Det som inte fungerar

Den vanligaste ”lösningen” är att skriva i systemprompten att modellen inte får generera skadlig kod.

Det är ingen säkerhetsåtgärd. Det är en artighetsfras. En prompt är indata, och indata kan påverkas — texten i kalkylbladet är också indata, och en cell kan innehålla instruktioner. Modellen kan aldrig vara säkerhetsgränsen, eftersom det är modellen som är den opålitliga komponenten.

Gränsen måste ligga i vad koden kan göra, inte i vad någon bett den låta bli.

Fyra lager, för att vart och ett går att ta sig förbi

Ett: läs koden som kod, inte som text. Innan något körs parsas den genererade koden till ett syntaxträd, och allt som importerar utanför en snäv lista avvisas — kalkylbladsbibliotek, textbehandling, några standardmoduler, punkt.

Att i stället söka efter textsträngen import os är meningslöst; det finns ett dussin sätt att nå samma modul utan att skriva orden. Ett syntaxträd beskriver vad koden faktiskt gör, inte hur den ser ut. Men även en vitlista går att kringgå via omvägar i språket — vilket är hela skälet till att det finns tre lager till.

Två: byt ut filhanteringen. Inne i sandlådan är den vanliga funktionen för att öppna filer ersatt med en egen som löser upp sökvägen och vägrar allt utanför två kataloger: en att läsa från och en att skriva till. Koden kan inte röra något annat på filsystemet, oavsett vad den försöker.

Tre: en container som inte kan så mycket. Det här är den riktiga gränsen, och den är byggd av operativsystemet snarare än av min kod:

  • Inget nätverk alls. Ingen utgående anslutning finns att göra. Även om koden fick tag i något känsligt kan den inte skicka det någonstans.
  • Skrivskyddat filsystem utom de två arbetskatalogerna. Ingenting kan lämnas kvar till nästa körning.
  • Kör som en användare utan rättigheter, med flaggan som gör att processen aldrig kan höja sina egna privilegier.
  • Minne, processorandel och antal processer är begränsade. En oändlig loop eller en processbomb tar ner sin egen container och ingenting annat.

Poängen med den uppräkningen är att varje rad stänger en utfallskategori, inte ett specifikt angrepp. Jag behöver inte förutse vad koden försöker göra.

Fyra: en människa ser skillnaden. Resultatet presenteras som en diff på cellnivå — exakt vilka rutor som ändrats, från vad till vad — innan användaren laddar ner något. Ingen fil ersätts automatiskt.

Det sista lagret är det jag skulle vara minst villig att offra. De tre första skyddar mot skadlig kod. Det fjärde skyddar mot något mycket vanligare: kod som är fullständigt ofarlig och gör helt fel sak.

1 · SYNTAXTRÄD — VITLISTA FÖR IMPORTER 2 · FILÅTKOMST BEGRÄNSAD TILL TVÅ KATALOGER 3 · CONTAINER: INGET NÄT, SKRIVSKYDDAD, UTAN RÄTTIGHETER Genererad kod antas vara fientlig 4 · MÄNNISKA Ser diffen före nedladdning Varje lager går att ta sig förbi. Det är därför de är fyra.
Fyra lager. Vart och ett går att ta sig förbi — det är därför de är fyra.

Fel sak är det troliga felet

Det är lätt att bli så upptagen av angreppsscenarier att man missar var risken faktiskt ligger i vardagen.

Ingen har försökt bryta sig ut ur sandlådan hos mig. Däremot har genererad kod tolkat ”ta bort de tomma raderna” som att den skulle ta bort rader som såg tomma ut men innehöll mellanslag. Den koden är harmlös enligt varje säkerhetsmodell, och den hade förstört ett underlag om ingen tittat.

Därför är diffen inte en trevlig detalj i gränssnittet. Den är den kontroll som fångar det som verkligen händer.

Så vet du att det funkar

Testa sandlådan genom att attackera den, inte genom att använda den.

  1. Be den uttryckligen om något förbjudet. ”Skriv kod som listar filerna i rotkatalogen.” Avvisas den innan körning fungerar det första lagret.
  2. Kringgå det första lagret med flit — nå en modul via en omväg i språket. Kommer koden igenom granskningen men misslyckas ändå, då är det containern som håller, vilket är precis meningen.
  3. Försök nå nätverket. Ett anrop utåt ska misslyckas direkt, inte hänga i en timeout. Hänger det är nätverket avstängt på fel sätt.
  4. Kör en oändlig loop och kontrollera att bara sandlådan påverkas medan resten av tjänsten svarar som vanligt.

Ett skydd du aldrig sett utlösa är ett skydd du inte vet något om.

Vad jag tar med mig

Sluta fråga om modellen är pålitlig. Fråga vad som händer när den inte är det. Kod som genereras av en modell ska behandlas precis som kod inskickad av en okänd användare över internet — för det är exakt vad den är, med ett extra led emellan.

Lager, för att varje enskilt lager går att ta sig förbi. Vitlistan går att lura. Filfunktionen skulle kunna ha ett hål. Containern är det starkaste ledet, men även den vilar på att kärnan gör sitt jobb. Att stapla dem gör att ett misstag i ett lager inte blir ett haveri.

Och den obekväma: allt det här kostade mer arbete än funktionen i sig. Att få en modell att skriva kod som ändrar ett kalkylblad tog en kväll. Att göra det så att jag vågar låta någon annan använda det tog betydligt längre. Det är, misstänker jag, den verkliga skiljelinjen mellan en demo och en produkt.

Skrivet av Daniel Azzarri · Om · Alla inlägg