O 131% więcej zgłoszonych CVE dziennie w 2026 niż przed rokiem.

Bezpieczeństwo PHP

Bezpieczeństwo PHP: jak zapobiegać podatnościom PHP w kodzie, zależnościach Composer i buildzie

Zabezpieczenie PHP to trzy warstwy. W kodzie, który Państwo piszą: walidacja danych wejściowych, zapytania parametryzowane i escaping frameworka, a także zasada, by nigdy nie deserializować niezaufanych danych. W pakietach Composer, od których Państwo zależą: uruchamianie composer audit, commitowanie composer.lock i blokowanie niebezpiecznych wersji. W CI, które je instaluje: rejestrowanie tego, co faktycznie zainstalowano, i ograniczanie, z czym build może się łączyć. Większość poradników o bezpieczeństwie PHP kończy się na pierwszej warstwie. Ten obejmuje wszystkie trzy.

Zaktualizowano

  1. Państwa kod Injection, XSS, deserializacja, obsługa plików, sesje Domyślne ustawienia frameworka, code review, PHPStan, Psalm, Semgrep
  2. Państwa zależności Composer Znane podatności, porzucone i złośliwe pakiety, dryf lockfile, kod uruchamiany podczas instalacji composer audit, polityki Composera, Dependabot lub Renovate
  3. Państwa build Co CI faktycznie zainstalowało i uruchomiło, skąd to pobrało, do czego miało dostęp Przypięte actions, tokeny o minimalnych uprawnieniach, polityka egress, SBOM z czasu buildu
Trzy warstwy bezpieczeństwa PHP i narzędzia, które działają w każdej z nich

Na tej stronie: PHP2Shell, podatności PHP, które wciąż mają znaczenie, konfiguracja i wspierane wersje, analiza statyczna, bezpieczeństwo zależności Composer, ataki na łańcuch dostaw PHP, build w CI, SBOM i narzędzia według warstwy.

PHP2Shell: podatność PHP do załatania od razu

21 października 2026 roku projekt PHP wydał PHP 8.5.12, 8.4.27, 8.3.36 i 8.2.35, aby naprawić dwie krytyczne podatności w samym PHP, które mogą umożliwić zdalne wykonanie kodu bez uwierzytelnienia. Obie mają ocenę 9,2 w CVSS v4.0 i razem są znane jako PHP2Shell:

  • CVE-2026-109618: obsługa braku pamięci może przerwać PHP, gdy dane żądania są zainicjalizowane tylko częściowo, a sprzątanie po żądaniu może potem potraktować te nieprawidłowe dane jako prawidłowe.
  • CVE-2026-109619: stan należący do żądania może przetrwać zwolnienie jego pamięci, a kolejne żądanie obsługiwane przez ten sam worker może z niego skorzystać.

Komunikat PHP mówi, że zagrożona jest każda instalacja obsługująca żądania webowe, w tym PHP-FPM, Apache z mod_php, php-cgi, FrankenPHP i LiteSpeed, a najwyższe ryzyko przypisuje mod_php. PHP 8.1 i starsze wersje nie dostają od projektu PHP nowego wydania. Ze względu na wagę błędów zespół bezpieczeństwa PHP opublikował jednak minimalne backporty kodu źródłowego dla PHP 7.4, 8.0 i 8.1, z myślą o tych, którzy budują PHP ze źródeł lub nie mają dostawcy LTS, a niektóre dystrybucje i dostawcy LTS dostarczają własne backportowane poprawki. Co zrobić:

  • Zaktualizować do poprawionego wydania. Na 8.1 lub starszej przejść na wspieraną gałąź, zainstalować pakiet z backportem od dostawcy albo zastosować oficjalny backport kodu źródłowego, jeśli PHP buduje się samodzielnie.
  • Przebudować każdy obraz kontenera zawierający PHP i wdrożyć go ponownie. Stary tag obrazu uruchamia stare PHP, dopóki nie zostanie zastąpiony, więc warto sprawdzić to, co jest wdrożone, a nie tylko to, co jest w repozytorium: docker run --rm IMAGE php -v.
  • Jeśli dziś nie da się zaktualizować, zastosować tymczasowe środki zaradcze z komunikatu bezpieczeństwa PHP, takie jak session.upload_progress.enabled=0. Ustawić je w php.ini, w puli FPM lub w konfiguracji serwera, która działa przed parsowaniem żądania, i zrestartować każdy worker. Nie polegać tu na ini_set() ani na .user.ini w FPM: komunikat przestrzega przed jednym i drugim. PHP jasno zaznacza, że te środki zmniejszają ekspozycję, ale nie czynią instalacji bezpieczną.

Proszę zwrócić uwagę, czego composer audit tu nie pokaże: PHP2Shell tkwi w runtime, a nie w pakiecie Composer, więc nigdy nie pojawia się w composer.lock. PHP, które Państwo uruchamiają, pochodzi z obrazu bazowego lub z pakietów systemu operacyjnego, czyli z trzeciej warstwy tego poradnika. Tabela wersji, oś czasu i skrypt sprawdzający każdy tag obrazu: PHP2Shell: co załatać i jak znaleźć każde podatne PHP.

Podatności PHP, które wciąż mają znaczenie

Klasy podatności w aplikacjach PHP prawie się nie zmieniły od dekady. Zmieniło się to, że Laravel, Symfony i nowoczesne PHP domyślnie zamykają większość z nich, więc prawdziwe błędy skupiają się dziś tam, gdzie kod omija te ustawienia domyślne. Injection to A05 w OWASP Top 10:2025, a Software Supply Chain Failures to nowość na pozycji A03, której poświęcona jest druga połowa tego poradnika.

Podatność Jak objawia się w PHP Poprawka Laravel i Symfony
SQL injection Dane z żądania doklejane do SQL, w tym przez furtki do surowych zapytań Prepared statements w PDO z powiązanymi parametrami Eloquent i Doctrine wiążą parametry; whereRaw(), DB::raw() i sklejany DQL nie
Cross-site scripting (XSS) Dane od użytkownika wypisywane w HTML bez escapingu Escaping na wyjściu przez htmlspecialchars() lub silnik szablonów Blade {{ }} i Twig domyślnie stosują escaping; {!! !!} i |raw nie
Cross-site request forgery (CSRF) Formularz lub endpoint AJAX zmieniający stan przyjmuje każde żądanie z ciasteczkiem sesji Token na sesję sprawdzany przy każdym POST, PUT, PATCH i DELETE, do tego ciasteczka SameSite Laravel sprawdza token na trasach web (@csrf w formularzach); Symfony Forms domyślnie dodają i sprawdzają token; trasy wyłączone ze sprawdzania nie mają ochrony
Code injection przez eval() Dane żądania albo szablon lub formuła, które użytkownik może edytować, trafiają do eval() Nie używać eval(); mapować dane wejściowe na stały zestaw operacji albo użyć parsera dla potrzebnych wyrażeń Żaden framework nie chroni Państwa własnych wywołań eval()
Object injection unserialize() na cookies, danych żądania lub wpisach cache, które atakujący może zapisać JSON dla danych z zewnątrz; jeśli deserializacja jest konieczna, odrzucać obiekty Żaden framework nie chroni Państwa własnych wywołań unserialize()
Dołączanie plików i uploady Ścieżka zbudowana z danych wejściowych i przekazana do include; uploady zapisywane pod web rootem pod nazwą pliku od klienta Allow-lista dołączanych plików; uploady poza web rootem pod generowanymi nazwami store() w Laravelu generuje nazwy plików; typ i rozmiar trzeba walidować na serwerze
Type juggling == przy porównywaniu tokenów lub hashy, gdzie "0e123" == "0e456" daje prawdę ===, hash_equals() dla sekretów, password_verify() dla haseł Helpery frameworka porównują bezpiecznie; własne porównania to Państwa odpowiedzialność
Słabe sesje i cookies Identyfikatory sesji czytelne dla JavaScriptu, wysyłane przez HTTP lub zachowywane po zalogowaniu Cookies Secure, HttpOnly i SameSite, tryb strict, nowy identyfikator sesji po zalogowaniu Oba regenerują identyfikator sesji przy logowaniu
Server-side request forgery (SSRF) file_get_contents() lub cURL na adresie URL od użytkownika, w tym wrappery takie jak file:// i phar:// Allow-lista schematów i hostów, blokada adresów wewnętrznych, wyłączone allow_url_fopen, gdzie nie jest używane Brak domyślnej ochrony
Command injection Dane wejściowe przekazywane do exec(), shell_exec(), system() lub proc_open() Unikać powłoki; przekazywać argumenty jako tablicę albo escapować każdy przez escapeshellarg() Symfony Process i fasada Process w Laravelu przyjmują tablicę argumentów
Mass assignment User::create($request->all()) pozwala żądaniu ustawić is_admin Wymienić pola do wypełnienia lub przekazywać tylko zwalidowane dane Modele Eloquent są domyślnie chronione; $guarded = [] wyłącza tę ochronę

SQL injection: wersja podatna i poprawiona

// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);

// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();

// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get();             // fixed

XSS: wersja podatna i poprawiona

<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>';                                              // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>';    // fixed ?>

{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p>   {{-- vulnerable --}}
<p>{{ $comment }}</p>     {{-- fixed --}}

{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p>  {# vulnerable #}
<p>{{ comment }}</p>      {# fixed #}

Object injection: wersja podatna i poprawiona

// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);

// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);

// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);

Deserializacja phar, czyli wariant, w którym operacja na pliku ze ścieżką phar:// deserializowała metadane archiwum, została ograniczona w PHP 8.0: metadane phar nie są już deserializowane przy otwieraniu pliku phar, tylko gdy kod wywoła getMetadata(). To kolejny powód, by zostawić PHP 7 za sobą.

Dołączanie plików: wersja podatna i poprawiona

// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';

// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;

Konfiguracja i utwardzanie runtime

Bezpieczna aplikacja wciąż może ujawniać informacje przez swój runtime. Poniższe ustawienia php.ini odpowiadają OWASP PHP Configuration Cheat Sheet:

Ustawienie Wartość produkcyjna Dlaczego
display_errors Off (z log_errors = On) Stack trace'y pokazują ścieżki, zapytania, a czasem sekrety
expose_php Off Przestaje ujawniać dokładną wersję PHP w nagłówkach odpowiedzi
allow_url_include Off Blokuje include zdalnych adresów URL; przestarzałe od PHP 7.4
allow_url_fopen Off, jeśli aplikacja nie pobiera adresów URL funkcjami plikowymi Zawęża SSRF przez file_get_contents()
disable_functions Na przykład exec, shell_exec, system, passthru, proc_open, popen Odbiera powłokę atakującemu, który zdołał uruchomić kod; proszę wpisać tylko to, czego aplikacja nigdy nie wywołuje
open_basedir Katalog aplikacji Ogranicza ścieżki, które mogą otwierać funkcje plikowe PHP; to zabezpieczenie pomocnicze, nie granica izolacji
Cookies sesji session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax lub Strict, session.use_strict_mode = 1 Trzyma identyfikatory sesji z dala od zwykłego HTTP i JavaScriptu oraz odrzuca identyfikatory, których serwer nie wydał
file_uploads Off, jeśli aplikacja nie przyjmuje uploadów Usuwa całą ścieżkę wejścia

Korzystanie ze wspieranej wersji PHP

Według php.net PHP zapewnia każdej gałęzi dwa lata aktywnego wsparcia, a potem dwa lata wyłącznie poprawek bezpieczeństwa. PHP 8.1 osiągnęło koniec życia 31 grudnia 2025 roku, a PHP 8.2 osiągnie go 31 grudnia 2026 roku. Gałąź po końcu życia nie dostaje od projektu PHP nowych wydań i właśnie w takiej sytuacji PHP2Shell postawiło użytkowników 8.1: jedyną oficjalną poprawką był dla nich backport kodu źródłowego.

  • PHP 8.1 Koniec życia, zakończone 31 gru 2025
  • PHP 8.2 Tylko poprawki bezpieczeństwa, do 31 gru 2026
  • PHP 8.3 Tylko poprawki bezpieczeństwa, do 31 gru 2027
  • PHP 8.4 Aktywne wsparcie, do 31 gru 2026
  • PHP 8.5 Aktywne wsparcie, do 31 gru 2027
Wspierane wersje PHP (daty z php.net)

Ustawienia produkcyjne w Laravelu i Symfony

  • Wyłączony debug. APP_DEBUG=false w Laravelu, APP_ENV=prod i APP_DEBUG=0 w Symfony. Strona debugowania na produkcji pokazuje konfigurację i wartości środowiskowe.
  • Klucze i sekrety. APP_KEY (Laravel) i APP_SECRET (Symfony) trzymać poza repozytorium i rotować w razie wycieku. APP_PREVIOUS_KEYS w Laravelu pozwala zrotować klucz bez wylogowywania wszystkich użytkowników.
  • Zaufane proxy. Za load balancerem należy wymienić zaufane proxy (middleware trusted proxies w Laravelu, framework.trusted_proxies w Symfony), aby klient nie mógł podrobić swojego adresu IP ani HTTPS nagłówkami X-Forwarded-*.

Analiza statyczna dla PHP (SAST)

Analiza statyczna czyta kod bez jego uruchamiania. Trzy narzędzia open source wystarczają większości zespołów PHP:

  • PHPStan znajduje błędy typów i bugi na kolejnych poziomach rygoru. Nie jest skanerem bezpieczeństwa, ale wiele błędów bezpieczeństwa zaczyna się od pomylenia typów albo niesprawdzonego null.
  • Psalm dodaje analizę taint: śledzi dane od źródeł takich jak $_GET i $_COOKIE do ujść takich jak echo, include i zapytania SQL, i zgłasza problemy takie jak TaintedSql i TaintedHtml. Obecny Psalm uruchamia ją razem ze sprawdzaniem typów; --taint-analysis włącza ją dla pojedynczego przebiegu, a --report=results.sarif zapisuje SARIF dla GitHub code scanning.
  • Semgrep obsługuje PHP jako język ogólnie dostępny, z regułami społeczności dla typowych błędów PHP i analizą przepływu danych między funkcjami w wersji komercyjnej.

SAST jest hałaśliwy na starszym PHP z przyczyn strukturalnych: dynamiczne include'y, zmienne zmiennych, metody magiczne takie jak __get() i __call() oraz konwencje frameworków, które przepuszczają dane przez kontenery i fasady, za którymi analizator nie nadąża. Narzędzie albo zgaduje i zgłasza fałszywe alarmy, albo się poddaje i przeoczy prawdziwe przepływy.

Wdrożenie na starej bazie kodu najlepiej działa jak zapadka. Dzisiejsze wyniki zapisać jako baseline (vendor/bin/phpstan analyse --generate-baseline zapisuje phpstan-baseline.neon; Psalm ma --set-baseline), build ma się wywracać tylko na nowych wynikach, a poziom podnosić w miarę spłacania baseline. Zespoły z wieloletnimi bazami kodu PHP mówią nam, że autofix oparty na AI dobrze radzi sobie z podbijaniem zależności, a nierówno z wynikami dotyczącymi kodu, dlatego wygenerowane poprawki do wyników SAST warto przeglądać równie starannie jak każdą inną zmianę. SAST i SCA odpowiadają na różne pytania; zob. SAST vs SCA.

Bezpieczeństwo zależności Composer

Aplikacja Laravel lub Symfony instaluje dziesiątki pakietów Composer, w większości przechodnich. W ostatnich wydaniach Composer zyskał prawdziwe funkcje bezpieczeństwa; ta sekcja opisuje Composer 2.11, aktualne wydanie.

composer audit

composer audit sprawdza zainstalowane pakiety pod kątem komunikatów bezpieczeństwa z API Packagist.org, które zbiera komunikaty ze źródeł takich jak GitHub Advisory Database i FriendsOfPHP/security-advisories. Zgłasza też porzucone pakiety, a od Composer 2.10 wersje pakietów oznaczone jako malware. Wszystkie opcje opisuje dokumentacja Composera.

composer audit                      # audit what is installed in vendor/
composer audit --locked             # audit composer.lock, whatever is in vendor/
composer audit --no-dev             # skip require-dev packages
composer audit --direct             # direct dependencies only (Composer 2.11)
composer audit --format=json        # table, plain, json or summary
composer audit --abandoned=report   # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low

Od Composer 2.10 kod wyjścia to 0, gdy nie ma problemów, i 1, gdy cokolwiek nie przeszło audytu, więc działa jako bramka w CI bez parsowania wyniku. Porzucone pakiety domyślnie oblewają audyt.

Zaakceptowane wyniki powinny trafić do composer.json, z uzasadnieniem, a nie do flagi w CI, o której nikt nie pamięta. Composer 2.10 zastąpił większość dawnej konfiguracji audit blokiem policy:

{
    "config": {
        "policy": {
            "advisories": {
                "ignore-id": {
                    "CVE-1234": "Not affected: we never call the vulnerable parser."
                }
            },
            "abandoned": {
                "audit": "report"
            }
        }
    }
}

Starsze klucze (audit.ignore, audit.abandoned, audit.block-insecure) nadal działają jako rozwiązanie zapasowe, dopóki brakuje odpowiedniej sekcji policy, ale są przestarzałe. Ustawienia komunikatów trzeba migrować naraz: gdy tylko istnieje policy.advisories, każdy klucz audit.* związany z komunikatami jest ignorowany.

Blokowanie niebezpiecznych wersji

Od Composer 2.9 (listopad 2025) composer update i composer require odrzucają wersje pakietów, które mają komunikat bezpieczeństwa. Było to ustawienie audit.block-insecure, a teraz jest to policy.advisories.block, domyślnie włączone. Composer 2.10 dodał blokowanie malware na podstawie feedu malware Packagist, które dotyczy także composer install. Composer 2.11 dodał opcjonalną politykę cooldown, która wstrzymuje świeżo wydane wersje na wybrany okres, na przykład "7 days", bo wiele złośliwych wersji zostaje wykrytych i usuniętych w ciągu godzin lub dni.

W CI liczą się dwa szczegóły. Po pierwsze, blokowanie na podstawie komunikatów działa przy rozwiązywaniu wersji: wersja, która już jest w composer.lock, nadal instaluje się przez composer install po opublikowaniu komunikatu, więc composer audit wciąż jest potrzebny jako bramka. Po drugie, --no-blocking wyłącza wszystkie polityki dla jednego polecenia, więc warto go poszukać w swoich pipeline'ach.

Przed Composer 2.9 typowym sposobem blokowania niebezpiecznych wersji był roave/security-advisories, pakiet deklarujący conflict z każdą znaną podatną wersją, zbudowany na podstawie GitHub Advisory Database i FriendsOfPHP. Nadal działa i nadal pomaga przy starszych wersjach Composera; w Composer 2.9 i nowszych wbudowane blokowanie obejmuje te same komunikaty bez dodatkowego pakietu.

Dyscyplina lockfile

  • Commitować composer.lock w aplikacjach, aby każde środowisko instalowało te same wersje.
  • Używać composer install w CI i przy wdrożeniach, nigdy composer update, który w locie rozwiązuje nowe wersje.
  • Instalować buildy produkcyjne z --no-dev, aby narzędzia testowe nigdy nie trafiły do wydania.
  • Uruchamiać composer validate --strict: kończy się błędem, gdy composer.lock jest nieaktualny względem composer.json.

Lockfile zapisuje to, co rozwiązał Composer. Zob. czego lockfile nie zapisuje.

Kod uruchamiany przy instalacji: pluginy, skrypty i pliki z autoloadu

Composer uruchamia kod zależności w większej liczbie miejsc, niż spodziewa się większość zespołów, co czyni go odpowiednikiem skryptów instalacyjnych npm w świecie PHP:

  • Pluginy działają wewnątrz samego Composera. Od Composer 2.2 ustawienie allow-plugins musi wymieniać każdy plugin, który może się uruchomić, a domyślnie nie dopuszcza żadnego. Każdy nowy wpis tam warto traktować jak nową zależność z dostępem do powłoki.
  • Skrypty uruchamiają się tylko z głównego composer.json, nie z zależności. Często jednak uruchamiają Państwa aplikację.
  • Pliki z autoloadu wykonują się przy każdym załadowaniu autoloadera. W standardowej aplikacji Laravel composer install kończy się poleceniem php artisan package:discover, które ładuje autoloader, więc files z autoloadu zależności wykonują się w Państwa jobie CI w trakcie instalacji.

Jeśli job nie potrzebuje pluginów ani skryptów, --no-plugins --no-scripts nie dopuszcza do uruchomienia kodu zależności podczas instalacji; potrzebne kroki można potem uruchomić jawnie.

Źródła repozytoriów i dependency confusion

Composer może instalować z Packagist.org, z prywatnych repozytoriów Composer, takich jak Private Packagist, z repozytoriów VCS (adresy URL Git) i ze ścieżek lokalnych. Mieszanie źródeł prywatnych i publicznych to teren dependency confusion: jeśli nazwa Państwa prywatnego pakietu istnieje też na Packagist.org z wyższą wersją, który pakiet trafi do projektu? W Composer 2 każde repozytorium jest kanoniczne domyślnie: gdy Composer znajdzie pakiet w repozytorium, nie szuka go niżej na liście, a Packagist.org jest zawsze ostatni. Ustawienie "canonical": false na repozytorium usuwa tę ochronę, więc należy to robić tylko świadomie. Repozytoria VCS pobierają to, co host Git serwuje dla danego tagu, dlatego warto przypiąć je do commita w lockfile i przeglądać ich zmiany.

Porzucone i nieutrzymywane pakiety

Starsze środowiska PHP niosą pakiety, których opiekunowie zajęli się czymś innym. Packagist oznacza pakiet jako porzucony, gdy opiekun to zgłosi, często wskazując następcę, a composer audit domyślnie oblewa na nim audyt. Wiele nieutrzymywanych pakietów nigdy jednak nie zostaje formalnie porzuconych. Proszę sprawdzić datę ostatniego wydania wszystkiego, co obsługuje dane wejściowe, uwierzytelnianie lub kryptografię, i zaplanować zamienniki, zanim wymusi to podatność.

Bezpieczne aktualizacje przy małej liczbie testów

Najtrudniejszy przypadek to duża, starsza aplikacja z podatną zależnością i niewieloma testami. Co działa:

  1. Zaktualizować pakiet, który wciąga podatny. Większość wyników jest przechodnia; polecenie composer why vendor/package pokazuje łańcuch, a najpierw warto zaktualizować pakiet wyżej w łańcuchu w ramach jego ograniczenia.
  2. Aktualizować jedną rzecz naraz. composer update vendor/package --with-dependencies utrzymuje zmianę na tyle małą, by dało się ją przejrzeć w diffie composer.lock.
  3. Najpierw smoke testy krytycznych ścieżek. Logowanie, płatność, główne endpointy API: garść testów end-to-end wyłapuje większość problemów po aktualizacji.
  4. Testy charakteryzujące wokół tego, czego dotyka aktualizacja. Zapisać, co kod robi dziś, aby zauważyć, kiedy aktualizacja to zmieni.
  5. Wdrażać etapami i obserwować wskaźniki błędów, zanim zmiana trafi do wszystkich klientów.

Czego nie widzi composer audit

Lockfile to plan, a nie zapis buildu. composer audit czyta vendor/ lub composer.lock, więc nie widzi:

  • Samego PHP i jego rozszerzeń, które pochodzą z obrazu bazowego lub pakietów systemu operacyjnego (PHP2Shell jest tego przykładem).
  • Rozszerzeń PECL i bibliotek systemowych zainstalowanych w obrazie Docker.
  • Narzędzi pobieranych przez CI za pomocą curl, uruchamianych actions oraz binariów pobieranych przez plugin lub skrypt.
  • Złośliwego kodu, który nie ma jeszcze komunikatu ani oznaczenia malware.

Te same ograniczenia dotyczą npm audit i pip-audit w ich ekosystemach.

Ataki na łańcuch dostaw PHP

Cztery incydenty pokazują, gdzie naprawdę trafiają ataki na łańcuch dostaw wymierzone w PHP i co wykryłoby każdy z nich.

Marzec 2021: złośliwe commity w php-src

28 marca 2021 roku do repozytorium php-src wypchnięto dwa złośliwe commity podpisane nazwiskami dwóch głównych programistów. Zespół PHP ogłosił, że przestanie utrzymywać własny serwer Git, a repozytoria na GitHubie staną się kanoniczne. Dalsze ustalenia wykazały, że commity wypchnięto przez HTTPS z uwierzytelnianiem hasłem, być może z danymi logowania z wykradzionej bazy użytkowników, a nie przez przejęty serwer. Co by to wykryło: nic w Państwa własnym pipeline. Celem był sam język, co przypomina, że runtime PHP też jest zależnością.

Kwiecień 2021: command injection w Composerze (CVE-2021-29472)

Błąd w obsłudze adresów URL repozytoriów Mercurial przez Composera umożliwiał command injection. Packagist podał, że Packagist.org i Private Packagist były teoretycznie narażone przez spreparowane adresy URL pakietów, że nie sądzi, by błąd został wykorzystany, i naprawił go w Composer 2.0.13 i 1.10.22. Co by to wykryło: aktualny Composer na maszynach deweloperskich i w obrazach CI. Menedżer pakietów jest częścią Państwa łańcucha dostaw.

Maj 2023: przejęcia kont na Packagist

1 maja 2023 roku atakujący zalogował się do czterech nieaktywnych kont Packagist.org hasłami, które wyciekły gdzie indziej. Konta te kontrolowały łącznie 14 pakietów, a atakujący skierował je na sforkowane repozytoria. Forki miały zmieniony opis i żadnego złośliwego kodu. Packagist przywrócił je następnego ranka. Co by to wykryło: commitowany composer.lock, ponieważ composer install korzysta ze źródeł zapisanych w lockfile, oraz przeglądanie diffów lockfile pod kątem nieoczekiwanych adresów URL, co zaleca sam Packagist. Dla opiekunów: unikalne hasła i uwierzytelnianie dwuskładnikowe.

2026: przejęte tagi w laravel-lang i intercom-php

W 2026 roku atakujący z wykradzionym dostępem opublikowali złośliwe wydania istniejących pakietów. Packagist wymienia intercom/intercom-php (30 kwietnia) i pakiety laravel-lang (22 maja) w swojej aktualizacji o bezpieczeństwie łańcucha dostaw. W przypadku laravel-lang tagi wersji wskazywały na commity w złośliwym forku, a ładunek, plik src/helpers.php ładowany przez autoloader Composera, pobierał stealer danych uwierzytelniających polujący na klucze chmurowe, klucze SSH i pliki .env. Aikido naliczyło 233 przejęte wersje. Co by to wykryło: po oznaczeniu polityka malware w Composer 2.10 blokuje te wersje nawet przy composer install; wcześniej okres cooldown trzyma z dala zupełnie nowe wersje, polityka egress nie pozwala ładunkowi dotrzeć do jego serwera, a zapis każdego buildu pokazuje, które buildy zainstalowały złą wersję.

Od tego czasu Packagist ogłosił, że odrzuca ponownie otagowane wersje stabilne, publikuje rejestr przejrzystości zmian własności i tagów oraz oznacza malware na Packagist.org od marca 2026 roku.

Zabezpieczanie buildu PHP w CI

Build to miejsce, w którym spotykają się Państwa kod, zależności i sekrety, więc to tam złośliwy pakiet wyrządza najwięcej szkód. Workflow GitHub Actions dla buildu produkcyjnego:

name: build
on: [push, pull_request]

permissions:
  contents: read            # least privilege for GITHUB_TOKEN

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - run: composer validate --strict

      - id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
      - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ hashFiles('composer.lock') }}

      - run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
      - run: composer audit --no-dev
  • Token o minimalnych uprawnieniach. permissions: contents: read nie pozwala przejętemu krokowi wypchnąć kodu ani wydań przez GITHUB_TOKEN. Zob. uprawnienia tokena GitHub.
  • Przypięte actions. Pełnego SHA commita nie da się przesunąć tak jak tagu; komentarz zachowuje czytelną wersję dla Dependabota lub Renovate.
  • Cache pobrań, nie vendor/. Cache pobrań Composera sprawia, że composer install w każdym jobie działa na podstawie lockfile. Przywrócony vendor/ jest przyjmowany bez sprawdzania, a cache może zostać zatruty między workflowami; zob. cache GitHub Actions.
  • Rejestrowanie tego, co zainstalowano. Lockfile mówi, co powinno zostać zainstalowane. SBOM zapisany z buildu mówi, co faktycznie zainstalowano, w tym runtime PHP, pakiety systemu operacyjnego i pobrania spoza Composera. Zob. SBOM z czasu buildu.
  • Ograniczenie, z czym build może się łączyć. Build z Composerem potrzebuje Packagist, GitHuba i Państwa prywatnego repozytorium. Polityka egress, która dopuszcza tylko je, nie pozwoli stealerowi wysłać tego, co znalazł.

Generowanie SBOM dla projektu PHP

Standardową opcją open source jest plugin CycloneDX dla Composera. Jako plugin wymaga wpisu w allow-plugins:

composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json

Czyta composer.json razem z zainstalowanymi pakietami lub composer.lock, więc dokładnie wymienia zależności Composer i nic poza nimi. Nie ma w nim runtime PHP, jego rozszerzeń, pakietów apt lub apk z obrazu bazowego ani narzędzi pobranych przez job. SBOM z czasu buildu rejestruje także je, ponieważ powstaje z tego, co job pobrał, a nie z plików Composera. Formaty i to, co SBOM powinien zawierać, opisuje strona czym jest SBOM.

Narzędzia bezpieczeństwa PHP według warstwy

Warstwa Narzędzia Co obejmują Czego nie widzą
Kod PHPStan, Psalm, Semgrep Bugi i błędy typów, skażone przepływy danych, znane niebezpieczne wzorce Podatnych zależności, runtime, buildu
Zależności composer audit, polityki Composera, roave/security-advisories, Dependabot lub Renovate Opublikowane komunikaty, porzucone i oznaczone pakiety, pull requesty z aktualizacjami Państwa własnego kodu i wszystkiego poza composer.lock
Obraz Skanery kontenerów Pakiety systemu operacyjnego i języka w zbudowanym obrazie Tego, co build pobrał i nie zachował, oraz obrazów, których nikt nie przeskanował
SBOM Plugin CycloneDX dla Composera Pakiety Composer jako SBOM CycloneDX Wszystkiego poza Composerem
Build CRACI To, co każdy job GitHub Actions faktycznie zainstalował, z Composera i każdego innego źródła, monitorowane w czasie Państwa kodu PHP (brak SAST); systemów CI innych niż GitHub Actions

Więcej o obrazach: skanowanie kontenerów. Mniej, ale lepszych wyników we wszystkich warstwach: jak ograniczyć szum podatności.

Bezpieczeństwo PHP i CRACI

CRACI to runner GitHub Actions, który rejestruje, co każdy job faktycznie instaluje. Wystarczy zmienić runs-on na craci, a każdy pakiet Composer pobrany przez job zostanie zapisany z jego package URL (pkg:composer/vendor/package), razem z pakietami z menedżera pakietów systemu operacyjnego, obrazami kontenerów, źródłami Git i innymi pobraniami w tym samym jobie, w tym z pakietami przywróconymi z cache CI. Każdy komponent w SBOM był w buildzie.

CRACI dopasowuje te komponenty do znanych podatności i ponownie sprawdza SBOM-y tego, co Państwo wydali, w miarę publikowania nowych komunikatów, więc CVE ujawnione w przyszłym roku nadal wskaże buildy, które je zawierają. Policy gates mogą zatrzymać build na podstawie wyników, w tym build zawierający konkretne CVE, a polityka egress ogranicza, z czym job może się łączyć.

CRACI nie skanuje Państwa kodu PHP: z założenia nie ma SAST. Do warstwy kodu warto zachować PHPStan, Psalm lub Semgrep. Proszę zobaczyć każdy pakiet, który instaluje Państwa build PHP.

Kiedy CRACI nie jest odpowiedzią

  • Wdrażają Państwo przez FTP lub na hosting współdzielony bez CI. Nie ma buildu do zarejestrowania. Warto zacząć od composer audit i commitowanego composer.lock.
  • Ryzyko leży głównie w Państwa własnym kodzie, na przykład w starszej aplikacji pełnej surowego SQL. Najpierw SAST, przeglądy i testy.
  • Buildy powstają poza GitHub Actions, w GitLab CI lub Bitbucket Pipelines. CRACI obsługuje dziś GitHub Actions; GitLab CI i Jenkins są na roadmapie.

Bezpieczeństwo PHP: najczęstsze pytania

Jak zabezpieczyć kod PHP?

Działać w trzech warstwach. W kodzie: walidować dane wejściowe, używać prepared statements, polegać na escapingu wyjścia we frameworku i nigdy nie deserializować niezaufanych danych. W zależnościach: commitować composer.lock, uruchamiać composer audit w CI i nie wyłączać blokowania niebezpiecznych wersji w Composerze. W buildzie: przypinać actions, ograniczać token, zawężać dostęp do sieci i rejestrować, co zainstalował job.

Jakie są najczęstsze podatności PHP?

SQL injection, cross-site scripting, cross-site request forgery, code injection przez eval(), object injection przez unserialize(), dołączanie plików i niebezpieczne uploady, type juggling z ==, słabe ustawienia sesji, SSRF przez wrappery URL, command injection i mass assignment. Podatne zależności Composer i niezałatane wersje PHP, jak błędy PHP2Shell, są równie częstą przyczyną incydentów.

Jaki jest najlepszy skaner bezpieczeństwa PHP?

Żadne pojedyncze narzędzie nie obejmuje całego bezpieczeństwa PHP. Do kodu warto użyć analizatora statycznego, takiego jak PHPStan, Psalm lub Semgrep, do zależności Composer composer audit, do obrazu skanera kontenerów, a do tego, co CI faktycznie zainstalowało, zapisu z czasu buildu.

Jak działa composer audit i czego nie widzi?

Sprawdza zainstalowane pakiety, lub composer.lock z --locked, pod kątem komunikatów z API Packagist.org i zgłasza pakiety porzucone oraz oznaczone jako malware. Zwraca 1, gdy cokolwiek nie przejdzie audytu. Nie widzi samego PHP, rozszerzeń PECL, pakietów systemu operacyjnego w obrazie ani niczego, co CI pobiera poza Composerem.

Jak blokować niebezpieczne pakiety Composer w CI?

Używać Composer 2.9 lub nowszego, który domyślnie blokuje wersje z komunikatami podczas update i require (policy.advisories.block). Ponieważ podatna wersja zapisana w lockfile nadal się instaluje, warto też uruchamiać composer audit po composer install i wywracać job na podstawie kodu wyjścia. Nigdy nie używać --no-blocking w CI.

Czy Laravel jest domyślnie bezpieczny?

W dużej mierze tak. Blade stosuje escaping wyjścia, Eloquent wiąże parametry zapytań, modele są chronione przed mass assignment, a identyfikator sesji jest regenerowany przy logowaniu. Te zabezpieczenia przestają działać, gdy kod używa {!! !!}, whereRaw() z danymi wejściowymi, $guarded = [] lub APP_DEBUG=true na produkcji albo gdy wycieknie APP_KEY.

Jak aktualizować zależności w starszej aplikacji PHP z małą liczbą testów?

Zaktualizować pakiet wyżej w łańcuchu, który wciąga podatny, po jednym pakiecie naraz, poleceniem composer update vendor/package --with-dependencies. Przetestować krytyczne ścieżki, dodać testy charakteryzujące wokół tego, czego dotyka aktualizacja, i wdrażać etapami.

Jak wygenerować SBOM dla projektu PHP?

Plugin CycloneDX dla Composera (cyclonedx/cyclonedx-php-composer) generuje go z pakietów Composer poleceniem composer CycloneDX:make-sbom. Wymienia tylko pakiety Composer. SBOM z czasu buildu rejestruje też runtime PHP, pakiety systemu operacyjnego i inne pobrania w buildzie.

Czym jest PHP2Shell?

PHP2Shell to nazwa CVE-2026-109618 i CVE-2026-109619, dwóch krytycznych podatności w samym PHP, które mogą umożliwić zdalne wykonanie kodu bez uwierzytelnienia. Naprawiają je PHP 8.5.12, 8.4.27, 8.3.36 i 8.2.35, wydane 21 października 2026 roku. Zob. PHP2Shell.

Proszę zobaczyć każdy pakiet, który instaluje Państwa build PHP

Proszę uruchomić jeden workflow GitHub Actions na CRACI i porównać zapisany SBOM z composer.lock.

Umów demo