Et workflow kører med dine repository-hemmeligheder, en token der kan skrive i din kode, og ofte adgangsoplysninger til din cloud-konto. Det er det mest privilegerede i hele repositoryet — og som regel det, der bliver gennemgået mindst.

Skævheden er værd at sige rent ud. En pull request, der ændrer applikationskode, bliver læst af en anden udvikler. En pull request, der ændrer en workflow-fil, bliver det ofte ikke, fordi workflow-YAML ligner konfiguration frem for kode. Det er kode, det kører med flere rettigheder end din applikation, og det er en direkte vej til din deployment-sti.

Ikke-fastlåste actions: du kører en andens seneste commit

Næsten alle workflows bruger tredjeparts-actions. Spørgsmålet er, hvad en reference reelt peger på.

At referere en action ved branch — @main — betyder, at din pipeline kører, hvad der end ligger på den branch, næste gang den udløses. Forfatteren kan ændre det når som helst, uden en udgivelse, og du bliver ikke spurgt. Bliver deres konto kompromitteret, kører din pipeline angriberens kode ved næste push, med dine hemmeligheder i miljøet.

Et tag som @v3 føles sikrere og er det kun en anelse. Git-tags kan flyttes: forfatteren kan lade v3 pege på en anden commit, og de færreste ville nogensinde opdage det.

Den eneste reference, der ikke kan ændre sig under dig, er en fuld commit-SHA:

uses: some-org/some-action@8f4b7c2e1d9a3f5b6c8e0d2a4f6b8c0e2d4f6a8b

Grimt og korrekt. Fastlås tredjeparts-actions på den måde, med en kommentar der nævner versionen, så et menneske kan læse det — og brug et værktøj til at holde fastlåsningerne opdateret bevidst frem for automatisk.

Et forbehold om gradering. At fastlåse førsteparts-actions som actions/checkout@v4 med SHA er forsvarligt — og langt mindre presserende. En scanner, der behandler det som lige så alvorligt, markerer stort set hvert eneste repository på GitHub — for at følge GitHubs egne dokumenterede eksempler. Det er sådan, et tjek bliver ignoreret. Gradér efter, hvor foranderlig referencen er, og hvem der kontrollerer den: en tredjeparts-branch er alvorlig, et tredjeparts-tag er moderat, et førstepartstag er mindre.

pull_request_target: den, der er reelt farlig

Denne udløser findes af en fornuftig grund og bliver jævnligt misbrugt på en måde, der overrækker repositoryet til enhver, der kan åbne en pull request.

Den almindelige pull_request-udløser kører et workflow mod en forks kode uden adgang til dine hemmeligheder — bevidst, fordi koden ikke kan stoles på. Det er upraktisk, når et workflow legitimt har brug for en hemmelighed, og derfor findes pull_request_target: det kører i basis-repositoryets kontekst, med dine hemmeligheder til rådighed og — afhængigt af repositoryets indstilling for workflow-rettigheder — en token, der kan skrive. Hemmelighederne alene er nok til, at mønsteret er farligt.

Den afgørende detalje er, at det er designet til at køre din workflow-fil, fra din default branch, ikke bidragyderens kode. Det bliver en sårbarhed i samme øjeblik, workflowet eksplicit checker pull requestens kode ud:

on: pull_request_target
steps:
  - uses: actions/checkout@v4
    with: { ref: '${{ github.event.pull_request.head.sha }}' }
  - run: npm install && npm test

Nu åbner hvem som helst i verden en pull request med et ondsindet post-install-script, og det bliver udført med dine hemmeligheder i miljøet. De har ikke brug for, at den bliver merget eller så meget som gennemgået — kun åbnet.

Har du brug for mønsteret, så kør ikke kode, du ikke stoler på, i det privilegerede job. Del det op i to workflows: et almindeligt pull_request-workflow bygger og tester bidragyderens kode uden hemmeligheder og lægger resultatet som et artifact, og et separat workflow_run-workflow henter artifaktet og gør det privilegerede. Det er GitHubs eget anbefalede mønster.

Script injection gennem workflow-udtryk

En mere subtil en. Workflow-udtryk bliver indsat i shell-scriptet, før det kører — tekstuelt, ikke som argumenter. Så det her:

- run: echo "Titel: ${{ github.event.pull_request.title }}"

bliver til en shell-kommando, der indeholder, hvad end nogen har skrevet som titel på deres pull request. En titel med en kommandosubstitution bliver udført på din runner. Det samme gælder branch-navne, issue-tekster og commit-beskeder — alt, en udefrakommende kontrollerer.

Løsningen er at sende værdien gennem en miljøvariabel, så shellen modtager den som data:

- env: { PR_TITLE: '${{ github.event.pull_request.title }}' }
  run: echo "Titel: $PR_TITLE"

Token-rettigheder: læseadgang som udgangspunkt

Den token, et workflow har til rådighed, kan have bred skriveadgang til repositoryet. De fleste workflows har brug for at læse kode og ikke andet.

Sæt permissions: { contents: read } øverst i workflowet, og giv kun mere til de bestemte jobs, der har brug for det. Et generelt write-all betyder, at ethvert kompromitteret trin — en ikke-fastlåst action, en indsprøjtet kommando, en ondsindet afhængighed i jeres eget build — kan pushe commits, ændre udgivelser eller åbne pull requests.

Hold helt op med at gemme langlivede cloud-nøgler

Alt ovenstående begrænser, hvad et kompromitteret workflow kan nå. Den dybere løsning er at gøre dét, det når, værdiløst i sig selv. GitHub Actions kan bede om en kortlivet OpenID Connect-token, som din cloud-udbyder veksler til midlertidige adgangsoplysninger afgrænset til ét bestemt repository og én branch. Så ligger der ingen permanent AWS- eller Azure-nøgle i repository-hemmelighederne, som en indsprøjtet kommando kan læse, og en stjålet token udløber på minutter.

En beslægtet fælde: en self-hosted runner tilknyttet et offentligt repository vil udføre kode fra enhver forks pull request på jeres eget hardware. GitHub dokumenterer det som ikke-understøttet af en grund. Offentlige repositories bør bruge hostede runners.

En YAML-detalje, der i stilhed knækker tjek

Værd at kende, hvis du selv skriver værktøjer: i YAML 1.1 — som de fleste parsere, inklusive SnakeYAML og PyYAML, stadig bruger som standard — bliver nøglen on uden anførselstegn tolket som den boolske værdi true. En parser, der leder efter en nøgle, der bogstaveligt hedder "on", finder ingenting og konkluderer, at workflowet ingen udløsere har — hvilket i stilhed gør "jeg kunne ikke analysere det her" til "intet at melde". Ethvert tjek, der læser workflow-filer, skal håndtere det, og vurderer du en scanner, er det et rimeligt spørgsmål at stille.

Hvorfor skal GitHub Actions fastlåses til en commit-SHA?

Fordi branch- og tag-referencer kan ændre sig under dig. At referere en action ved branch betyder, at din pipeline kører, hvad der end ligger på branchen næste gang den udløses, og git-tags kan flyttes, så en forfatter kan lade v3 pege på en anden commit, uden at nogen opdager det. En fuld commit-SHA er den eneste reference, der ikke kan ændre sig. Bliver forfatterens konto kompromitteret, betyder en ikke-fastlåst reference, at din pipeline kører angriberens kode med dine hemmeligheder i miljøet.

Hvad gør pull_request_target farlig?

pull_request_target-udløseren kører med dine repository-hemmeligheder til rådighed — og, afhængigt af repositoryets indstilling for workflow-rettigheder, en token der kan skrive — i modsætning til den almindelige pull_request-udløser, der bevidst holder dem tilbage fra fork-kode. Den er designet til at køre din workflow-fil, ikke bidragyderens kode, så den bliver en alvorlig sårbarhed, når workflowet eksplicit checker pull requestens kode ud og derefter bygger eller tester den — på det tidspunkt kan enhver, der kan åbne en pull request, udføre kode med dine hemmeligheder. Den behøver hverken at blive merget eller gennemgået, kun åbnet.

Hvad er script injection i et GitHub Actions-workflow?

Workflow-udtryk bliver indsat tekstuelt i shell-scriptet, før det kører, så en værdi, en udefrakommende kontrollerer — en pull request-titel, et branch-navn, en issue-tekst — bliver en del af kommandoen. En titel med en kommandosubstitution bliver udført på din runner. Løsningen er at sende værdien gennem en miljøvariabel, så shellen modtager den som data frem for som en del af selve scriptteksten.

Hvilke rettigheder bør et GitHub Actions-workflow have?

Begynd med contents: read på workflow-niveau, og giv kun mere til de bestemte jobs, der reelt har brug for det. De fleste workflows skal kun læse kode. Et generelt write-all betyder, at ethvert kompromitteret trin — en ikke-fastlåst action, en indsprøjtet kommando, en ondsindet afhængighed trukket ind under bygningen — kan pushe commits, ændre udgivelser eller åbne pull requests med workflowets egen token.

Hvor CodeControl passer ind

CodeControl gennemgår workflow-filerne i dine forbundne repositories for præcis de her problemer. Ikke-fastlåste actions graderes efter, hvor foranderlig referencen er, og hvem der kontrollerer den, så det ikke lyser din rapport op at følge GitHubs egne eksempler; pull_request_target kombineret med et checkout af pull requestens kode behandles som kritisk; mønstre for script injection og for brede token-rettigheder meldes med den præcise linje. Det kører på samme scanning som afhængighedsfortegnelsen og detektion af hemmeligheder.

Er det en AI-agent, der har den token, flytter rettighedsspørgsmålet sig et trin op: hvad agenten må nå, og hvem der har taget stilling.