Od kilku dni moje feedy security zdominował temat związany z zakończonym sukcesem atakiem na łańcuch dostarczania oprogramowania z wykorzystaniem GitHub Actions – https://thehackernews.com/2025/03/github-action-compromise-puts-cicd.html

Aby opisać, na czym polega wspomniany atak, warto zrozumieć, jak działa mechanizm GitHub Actions.

Z procesem CI/CD zdążyliśmy się oswoić w ciągu ostatnich kilku lat. Rozwiązań, które pozwalają na automatyzację operacji budowania i wdrażania aplikacji w oparciu o zdarzenia takie jak np. zmiana w kodzie źródłowym (git push event) jest całkiem sporo. Zaczynając od jednego z pierwszych rozwiązań wykorzystywanych szeroko – Jenkins, po rozwiązania SaaS, które na naszym polskim podwórku są trochę mniej popularne, takie jak: CircleCI czy Travis, kończąc na najpowszechniej wykorzystywanych GitHub Actions i GitLab CI.

źródło: https://dev.to/pavanbelagatti/learn-how-to-setup-a-cicd-pipeline-from-scratch-for-a-go-application-4m69

Z czego skłąda się Github Actions?

W dużym uproszczeniu proces CI/CD składa się z kilku elementów, które są określane na etapie jego budowania czy projektowania:

  • Wyzawalacz (trigger) – określa to, kiedy proces ma się uruchomić np. gdy ktoś wyśle kod do repozytorium (push), gdy ktoś utworzy prośbę o włączenie zmian (pull request) lub według jakiegoś harmonogramu (scheduler, codziennie o 12:00)
  • Przepływ pracy (workflow/pipeline) – określa serię kroków, które mają być wykonane z dokładnością do kolejności czy tego, które z nich mogą być realizowane równolegle
  • Akcja/Zadanie – poszczególne zadanie w przepływie pracy

W zależności od wyboru narzędzia różnica głównie polega na sposobie reprezentacji konfiguracji za pomocą kodu źródłowego. Przykład prostego workflow z wykorzystaniem GitHub Actions może wyglądać tak:

name: Testowanie kodu

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
      
    - name: Instalacja zależności
      run: npm install
      
    - name: Uruchomienie testów
      run: npm test

Mapując ten przykład na składowe opisane powyżej:

  • wyzwalaczem są akcje push i pull request na gałęzi main
  • Przepływ jest jeden i składa się z 3 akcji, które wykonywane są jedna po drugiej
  • Akcje to:
    • pobranie kodu z wykorzystaniem gotowej akcji actions/checkout@v3
    • uruchomienie npm install w warstwie obrazu ubuntu-latest
    • uruchomienie npm test w warstwie obrazu ubuntu-latest

Typy akcji w Github Actions

Pierwszym typem akcji jest własne wykonanie powłoki, gdzie możesz wykonywać dowolne polecenie dopasowując je do swoich potrzeb.

steps:
  - name: Instalacja NPM i testy
    run: |
      npm install
      npm test
      echo "Testy zakończone pomyślnie!"

Mechanizm ten pozwala na zbudowanie bardzo elastycznych procesów przy założeniu, że wszystkie potrzebne komendy systemowe są dostępne w obrazie, z którego dany workflow korzysta.

Drugi typ to gotowe akcje, czyli predefiniowane zadania, które możesz wykorzystać w swoim przepływie pracy. Na przykład:

steps:
  - uses: actions/checkout@v3
  - uses: actions/setup-node@v3
    with:
      node-version: '16'

W tym przykładzie używane są dwie gotowe akcje:

  • actions/checkout@v3 – pobiera kod z repozytorium
  • actions/setup-node@v3 – instaluje Node.js w wersji 16 (wzięte z dynamicznych ustawień)

Te akcje to nic innego jak odniesienie do repozytorium: actions/checkout@v3 -> https://github.com/actions/checkout. W podobny sposób w swoim procesie CI/CD jesteśmy w stanie wykorzystać gotową akcję, która została udostępniona przez inne repozytorium, do którego mamy dostęp (lub inne publiczne repozytorium). Jest to bardzo popularny mechanizm, za pomocą którego programiści nie muszą tworzyć takich samych przepływów, a wykorzystują te, które już ktoś wcześniej zrobił. W największym stopniu dotyczy to zadań o odrobinę większym poziomie skomplikowania niż wspominany npm install i npm test.

Akcja tj-actions/changed-files

Istnieje kilka akcji, którymi podzielili się inni programiści, a które cieszą się dużą popularnością, wśród nich jest np. peaceiris/actions-gh-pages oraz tj-actions/changed-files

Ta druga pozwala wykryć, które pliki zostały zmienione w konkretnej zmianie (po operacji push lub pull request). Jest to bardzo przydatne, ponieważ:

  • Optymalizuje czas wykonania – możesz uruchamiać tylko te testy, które dotyczą zmienionych plików
  • Oszczędza zasoby – nie musisz budować całego projektu, jeśli zmiany dotyczą tylko dokumentacji
  • Pozwala na inteligentne wyzwalanie workflow – różne zadania mogą być uruchamiane w zależności od tego, które pliki zostały zmienione

Z tej akcji korzystało ponad 23000 repozytoriów.

Nieautoryzowana zmiana w kodzie tj-actions/changed-files

Dochodzimy tutaj do problemu, który dotknął niemal wszystkie projekty korzystające z tej akcji. Wykryto, że przed 14 marca 2025 dokonano nieautoryzowanej zmiany w kodzie repozytorium, w którym znajdowała się definicja akcji. Zmiana ta skutkowała tym, że po uruchomieniu akcja wyświetlała wszystkie sekrety i hasła, które zostały zapisane i były wykorzystywane przez konkretne repozytorium w logach zadania.

źródło: https://thehackernews.com/2025/03/github-action-compromise-puts-cicd.html

Wyobraź sobie scenariusz, w którym:

  1. Wypychasz kod do repozytorium
  2. Sprawdzasz, czy zmiana dotyczy zmian w katalogu application
  3. Jeśli tak, zaczynasz testy aplikacji, a następnie uruchamiasz proces budowania obrazu dockerowego (docker build -t application .). Na tym etapie Twój proces CI/CD potrzebuje hasła do rejestru dockerowego (lub krótko żyjącego tokena), aby mieć uprawnienia do wypchnięcia zbudowanego obrazu do rejestru.
  4. Wypychasz obraz

Wykorzystując tj-actions/changed-files w zarażonej wersji, hasło lub token do rejestru zostanie umieszczone w logach akcji. Taki sekret może być dalej wykorzystany przez atakującego w celu modyfikacji lub podmienienia obrazu z Twoją aplikacją na obraz z “bonusem”. W kilku wpisach na moim blogu opisuję, dlaczego jest to bardzo poważny incydent.

Jak doszło do incydentu

Zespół utrzymujący i rozwijający nieszczęśliwe repozytorium opisał, że do nieautoryzowanej zmiany w repozytorium doszło w wyniku wykorzystania tokena osobistego (PAT – Personal Access Token) wykorzystywanego przez konto @tj-actions-bot, który z uwagi na swoje zadania miał uprzywilejowany dostęp do repozytorium. Jak atakujący weszli w posiadanie tego tokena? Do tej pory jeszcze nie znalazłem informacji na ten temat.

Korzystam z Gitlab CI – czy jestem bezpieczny?

Wskazane zagrożenie dotyczy tylko repozytoriów i projektów, które do procesu CI/CD wykorzystują GitHub Actions. Nie oznacza to jednak, że użytkownicy innych rozwiązań nie mają się o co martwić. Ataki na proces dostarczania oprogramowania (Supply chain attacks) z roku na rok stają się coraz bardziej popularne i z każdym kolejnym rokiem liczba narażonych systemów/repozytoriów rośnie. Nawet jeśli w procesie CI/CD realizujesz wszystkie możliwe testy bezpieczeństwa takie jak:

  • Software Component Analysis – wykryje podatności i zagrożenia w bibliotekach np. npm. Potrafi również wykryć zagrożenia w bibliotekach wbudowanych w finalny obraz dockerowy, który został zbudowany
  • Static Application Security Testing – wykryje zagrożenia w stworzonym kodzie aplikacyjnym
  • Secret Leaks – wykryje potencjalne wycieki haseł
  • Dynamic Application Security Testing – wykryje zagrożenia w działającej aplikacji

W dalszym ciągu nie przetestujesz ani nie zweryfikujesz tego, co jest uruchamiane w celu wykonania akcji budowania. Nie widziałem jeszcze zespołu ani przepływu CI/CD, który oprócz testów bezpieczeństwa aplikacji testował bezpieczeństwo samego siebie – ba, sam tego w żadnym projekcie nie robiłem i to sprowokowało mnie do powstania tego tekstu. W przypadku wykorzystania gitlab-ci przepływ może wyglądać tak:

image: python:3.9

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"

cache:
  paths:
    - .pip-cache/

stages:
  - test
  - build
  - deploy

test_app:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest tests/

build_image:
  stage: build
  image: docker:20.10.16
  services:
    - docker:20.10.16-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG .
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
  only:
    - main
    - tags

deploy_to_staging:
  stage: deploy
  image: alpine:latest
  script:
    - apk add --no-cache curl
    - curl -X POST "https://deployment-webhook.example.com/deploy" 
      -H "Authorization: Bearer $DEPLOY_TOKEN" 
      -d '{"image": "'$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG'"}'
  only:
    - main

Możemy tutaj wdrożyć wszystkie mechanizmy testowania aplikacji, ale zdadzą się one na nic, jeśli atakujący w skuteczny sposób przemycą niechciany kod do obrazu bazowego (w tym przypadku): python:3.9. Podobnie jak w przypadku GitHub Actions, zagrożenie może dotyczyć zarówno oficjalnych obrazów bazowych, jak i zewnętrznych zależności wykorzystywanych w pipeline’ach.

Co robić, jak żyć?

Wiedzieliście, że OWASP publikuje od jakiegoś czasu listę TOP 10 dla CI/CD? Warto się z nią zapoznać, a przede wszystkim:

  • Chronić hasła i tokeny, szczególnie dla kont, które mają podniesione uprawnienia. Każdy musi mieć włączone 2FA na pracę z repozytorium!
  • Testować, testować, i jeszcze raz testować aplikacje pod każdym kątem w procesie CI/CD. W kolejnym wpisie zaproponuję kilka sposobów na realizację testów samego procesu budowania.
  • Weryfikować, co uruchamiasz w procesie CI/CD i uruchamiać wyłącznie zaufany kod.

Aby dodatkowo zabezpieczyć swój pipeline, warto rozważyć następujące praktyki:

  • Pinowanie do konkretnych wersji akcji/obrazów z dokładnością do commita (np. actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f zamiast actions/checkout@v3)
  • Wdrożenie zasady „najmniejszych uprawnień” (principle of least privilege) dla wszystkich integracji i tokenów
  • Regularne audyty wykorzystywanych akcji/obrazów i zewnętrznych zależności
  • Tworzenie własnych, wewnętrznych i zaufanych akcji dla krytycznych części pipeline’u
  • Implementacja mechanizmów wykrywania anomalii w logach CI/CD
  • Automatyczna rotacja tokenów dostępowych
  • Ograniczenie dostępu do sekretu tylko do niezbędnych jobs/stages w pipeline

Pamiętaj, że bezpieczeństwo łańcucha dostaw oprogramowania to proces ciągły, wymagający stałego nadzoru i doskonalenia. Incydent z tj-actions/changed-files jest doskonałym przykładem tego, jak pozornie niegroźna, popularna i szeroko stosowana komponent może stać się wektorem ataku na Twoją infrastrukturę.

Tags: