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
- Państwa kod Injection, XSS, deserializacja, obsługa plików, sesje Domyślne ustawienia frameworka, code review, PHPStan, Psalm, Semgrep
- 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
- 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
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 wphp.ini, w puli FPM lub w konfiguracji serwera, która działa przed parsowaniem żądania, i zrestartować każdy worker. Nie polegać tu naini_set()ani na.user.iniw 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
Ustawienia produkcyjne w Laravelu i Symfony
-
Wyłączony debug.
APP_DEBUG=falsew Laravelu,APP_ENV=prodiAPP_DEBUG=0w Symfony. Strona debugowania na produkcji pokazuje konfigurację i wartości środowiskowe. -
Klucze i sekrety.
APP_KEY(Laravel) iAPP_SECRET(Symfony) trzymać poza repozytorium i rotować w razie wycieku.APP_PREVIOUS_KEYSw 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_proxiesw Symfony), aby klient nie mógł podrobić swojego adresu IP ani HTTPS nagłówkamiX-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
$_GETi$_COOKIEdo ujść takich jakecho,includei zapytania SQL, i zgłasza problemy takie jakTaintedSqliTaintedHtml. Obecny Psalm uruchamia ją razem ze sprawdzaniem typów;--taint-analysiswłącza ją dla pojedynczego przebiegu, a--report=results.sarifzapisuje 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.lockw aplikacjach, aby każde środowisko instalowało te same wersje. -
Używać
composer installw CI i przy wdrożeniach, nigdycomposer 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, gdycomposer.lockjest nieaktualny względemcomposer.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-pluginsmusi 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 installkończy się poleceniemphp artisan package:discover, które ładuje autoloader, więcfilesz 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:
-
Zaktualizować pakiet, który wciąga podatny. Większość wyników jest przechodnia; polecenie
composer why vendor/packagepokazuje łańcuch, a najpierw warto zaktualizować pakiet wyżej w łańcuchu w ramach jego ograniczenia. -
Aktualizować jedną rzecz naraz.
composer update vendor/package --with-dependenciesutrzymuje zmianę na tyle małą, by dało się ją przejrzeć w diffiecomposer.lock. - 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.
- Testy charakteryzujące wokół tego, czego dotyka aktualizacja. Zapisać, co kod robi dziś, aby zauważyć, kiedy aktualizacja to zmieni.
- 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: readnie pozwala przejętemu krokowi wypchnąć kodu ani wydań przezGITHUB_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, żecomposer installw każdym jobie działa na podstawie lockfile. Przywróconyvendor/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 auditi commitowanegocomposer.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