25 sierpnia 2026 zespół Next.js opublikował aktualizację bezpieczeństwa obejmującą dwie podatności klasy Critical prowadzące do unauthenticated Remote Code Execution. Na pierwszy rzut oka komunikat dla VM jest prosty: Next.js ma dwa Critical RCE, zaktualizuj do 15.5.24 albo 16.3.3. To prawidłowa rekomendacja — ale dla Vulnerability Engineera to dopiero początek.
Pierwsza luka — CVE-2026-75604 — jest związana z samym Next.js, lecz jej realna exploitability zależy od systemu operacyjnego, sposobu hostowania, używanych routerów i mechanizmu cache. Druga — GHSA-2xp9-vwfh-vxw4 — prowadzi kilka warstw niżej w łańcuchu zależności: Next.js korzysta z sharp do optymalizacji obrazów, a podatny problem jest w bibliotece libheif i wyzwala się przy przetwarzaniu spreparowanych plików AVIF. Jeden security release, dwa Critical RCE — ale zupełnie różne pytania: czy aplikacja działa na Windows? czy używa wymaganej architektury routerów? czy aktywny jest odpowiedni cache? czy korzysta z Image Optimization API? czy realnie przetwarza AVIF? czy atakujący może dostarczyć kontrolowany obraz? czy podatność jest w frameworku, czy w zależności transytywnej? kto jest właścicielem remediation? To ciekawsze niż sam numer CVSS — i to jest prawdziwy Vulnerability Engineering.
Co wydarzyło się 25 sierpnia?
Next.js opublikował wersje 15.5.24 (Maintenance LTS) i 16.3.3 (Active LTS), rozwiązujące dwie podatności Critical. Pierwsza — CVE-2026-75604 / GHSA-p293-qw3h-jr36: unauthenticated RCE on Windows-hosted servers. Druga — GHSA-2xp9-vwfh-vxw4: unauthenticated RCE in Image Optimization API when AVIF files are used. Na poziomie klasyfikacji oba wyglądają groźnie: zdalny atak bez uwierzytelnienia, skutek to wykonanie kodu, w obu vendor zaleca upgrade. Ale podobieństwa szybko się kończą.
Przypadek 1: CVE-2026-75604 — Critical, ale nie dla każdego deploymentu
GitHub Security Advisory opisuje CVE-2026-75604 jako unauthenticated RCE na serwerach hostowanych z wykorzystaniem Windows filesystem.
Ale najważniejszym fragmentem advisory nie jest 9.0. Najważniejszy jest warunek opisujący affected application: vendor wskazuje aplikacje korzystające z Pages Router i App Router bez Cache Components, hostowane na systemie z Windows filesystem. I tu zaczyna się triage.
Załóżmy, że SCA zwraca: next 16.2.11, CVE-2026-75604, CRITICAL, CVSS 9.0, fixed 16.3.3. Na poziomie package matching wszystko jest poprawne — wersja mieści się w vulnerable range. Ale czy aplikacja naprawdę jest exploitable? Nie wiemy. Musimy ustalić co najmniej: OS, hosting model, filesystem, Pages Router, App Router, Cache Components, relevant cache behavior, reachability. To fundamentalna różnica między version vulnerable a deployment exploitable.
Jeśli warunkiem exploita jest Windows filesystem, to deployment działający wyłącznie na Linuxie nie spełnia jednego z kluczowych warunków. SCA nadal może poprawnie powiedzieć „next@16.2.11 w affected range", ale VE powinien dodać: Runtime OS: Linux · Exploit precondition: Windows filesystem · Environmental exploitability: not demonstrated. Czy taki finding nadal powinien być czerwonym Critical P1? Nie ma jednej odpowiedzi — organizacja może patchować wszystko niezależnie od kontekstu, obniżyć priorytet dla deploymentów niespełniających warunków, oznaczyć je jako not affected po udokumentowanej analizie, zostawić finding otwarty z innym SLA albo utworzyć VEX. Najgorszym rozwiązaniem byłoby ignorowanie informacji o runtime.
Czy to jest przypadek dla VEX?
Potencjalnie tak. VEX — Vulnerability Exploitability eXchange — istnieje właśnie po to, aby powiedzieć więcej niż „SBOM zawiera komponent X". Możemy mieć komponent formalnie w affected range, a mimo to produkt może nie być realnie exploitable, bo podatny kod nie jest wykonywany, funkcja jest wyłączona, komponent jest poza attack path, platforma nie spełnia warunku albo wdrożono mitigation. Deployment Linux-only jest tu bardzo dobrym przykładem. Ale VEX nie powinien powstawać automatycznie na podstawie jednej obserwacji — najpierw potrzebujemy dowodu i jasno zapisanej podstawy decyzji:
Dopiero później organizacja może zatwierdzić formalny status. Ten sam numer CVE, różny priorytet na różnych assetach — wyobraźmy sobie cztery deploymenty:
Wszystkie cztery mogą pojawić się w jednym raporcie SCA. Ale nie powinny zakończyć triage z identyczną decyzją.
Przypadek 2: Next.js → sharp → libheif → AVIF
Advisory GHSA-2xp9-vwfh-vxw4 opisuje unauthenticated RCE w Image Optimization API (Next.js podaje Severity Critical, CVSS v4 9.5; poprawione wersje ponownie 15.5.24 / 16.3.3). Najważniejsze jest źródło problemu: podatność jest w underlying libheif używanej przez sharp, z którego Next.js korzysta przy optymalizacji obrazów. Finding, który developer widzi jako „next: vulnerable", technicznie wynika z łańcucha:
Upstream advisory libheif opisuje problem jako heap buffer overflow w scale_nearest_neighbor(). Odpowiednio przygotowany plik HEIC/HEIF/AVIF może doprowadzić do zapisu poza buforem podczas dekodowania; aplikacje korzystające z heif_decode_image() mogą być dotknięte; poprawka jest w libheif 1.23.2. Problem nie powstał dlatego, że developer napisał niebezpieczny endpoint — pojawił się kilka warstw niżej.
Kto jest właścicielem podatności?
To pytanie brzmi organizacyjnie, ale ma techniczne konsekwencje. Może istnieć wiele zespołów: aplikacyjny, platformowy (Next.js), budujący obrazy, dependency governance, Security wykrywający finding. Kto dostaje ticket? Jeśli SCA wskaże libheif vulnerable, ticket może trafić do zespołu obrazu; jeśli next vulnerable — do developera. A jeśli libheif przychodzi jako prebuilt/native dependency używana przez sharp, developer może nawet nie wiedzieć, że jest ona w runtime. To jeden z najważniejszych problemów nowoczesnego VM: ownership nie zawsze pokrywa się z miejscem, w którym wykryliśmy podatność.
Dlatego klasyczny SBOM („co jest w produkcie?") trzeba uzupełnić o „dlaczego ten komponent tu jest?". Idealny system pokazuje ścieżkę:
Taka ścieżka jest dużo bardziej użyteczna niż lista komponentów bez relacji — bo remediation może odbywać się na różnych poziomach drzewa.
Vendor zrobił coś ciekawego: wyłączył AVIF
Next.js poinformował, że do czasu pełnego rozpropagowania poprawki optymalizacja AVIF została wyłączona. To świetny przykład compensating control / feature mitigation. Nie zawsze pierwszą odpowiedzią musi być „replace vulnerable library immediately" — czasem możliwe jest „disable vulnerable attack surface". W tym przypadku AVIF optimization → disabled zmienia reachability podatnego kodu. To prowadzi do pytania: czy finding jest nadal „Open Critical", jeśli podatna biblioteka istnieje, ale funkcja prowadząca do exploita została skutecznie wyłączona?
„Vulnerable" nie jest jednym stanem. Dojrzały system VM powinien odróżniać co najmniej: VULNERABLE / MITIGATED / NOT_AFFECTED / PATCHED / RISK_ACCEPTED / FALSE_POSITIVE / UNKNOWN. Możemy mieć bibliotekę nadal obecną (SCA ją znajdzie), ale gdy attack surface został wyłączony, stan ryzyka nie jest identyczny. To nie znaczy, że rezygnujemy z patcha — znaczy, że system powinien umieć powiedzieć: „podatność jest obecna komponentowo, ale exploitation path jest obecnie zablokowany przez mitigation".
SCA widzi dependency. VE musi zobaczyć reachability
Nie każda aplikacja Next.js wykorzystuje next/image, optymalizuje obrazy, pozwala na zewnętrzne źródła, przetwarza AVIF ani pozwala attackerowi kontrolować obraz. Możemy więc mieć:
albo:
To dwie zupełnie inne sytuacje. SCA jest świetne w pytaniu „czy mam komponent w vulnerable range?"; trudniejsze pytanie brzmi „czy attacker może doprowadzić do wykonania podatnego kodu w mojej aplikacji?". To wymaga dodatkowych danych: runtime telemetry, konfiguracja frameworka, analiza kodu, routing, service inventory, feature flags, application security testing, ręczna analiza. Dlatego przyszłość VM przesuwa się od component matching do exploitability validation.
Dwa Critical RCE, dwa zupełnie inne grafy exploitability
Samo Severity = Critical nie pokazuje żadnego z tych grafów. Czy oba findings powinny mieć taki sam SLA? Może tak — vendor zaleca upgrade i dla internet-facing aplikacji szybka aktualizacja jest racjonalna. Ale decyzja dotycząca setek aplikacji może wymagać priorytetyzacji. Organizacja z 350 aplikacjami Next.js po release dostanie 700 Critical findings (dwa na aplikację). Wysłanie 700 identycznych P1 jest możliwe — ale lepszy jest automatyczny enrichment:
To zupełnie inna jakość danych niż CRITICAL / CRITICAL.
„Not affected in context" to nie „false positive"
Załóżmy aplikację: 16.2.11, Linux, App Router only, image optimization disabled, no AVIF. Scanner nadal wykryje obie podatności na podstawie wersji. Czy to false positive? Nie — to zbyt mocne. Scanner poprawnie wykrył komponent w affected range. Lepszym określeniem jest component affected / deployment not exploitable under current configuration. To różnica między false positive a not affected in context. False positive to „narzędzie twierdzi, że podatność jest obecna, ale technicznie jej nie ma" (np. źle rozpoznana wersja). Natomiast „vulnerable package jest obecny, ale wymagany feature jest wyłączony" to inna sytuacja — finding poprawny komponentowo, ale nieprowadzący do exploitable state w danym środowisku.
Dlatego „zamknij jako false positive" bywa niebezpieczne. Jeśli zespół zamknie CVE-2026-75604 jako „False Positive, reason: Linux", a pół roku później aplikacja przejdzie na Windows i nie ma mechanizmu re-evaluation, stare zamknięcie zostanie. Dużo lepszy jest stan Not affected under current deployment conditions z re-check triggerem (if runtime_os changes: reopen assessment). Analogicznie dla AVIF: Not exploitable because AVIF optimization disabled + if AVIF enabled: reopen assessment. To dynamiczny kontekst podatności zamiast statycznego wyjątku. VEX bez mechanizmu ponownej oceny może stać się długiem technicznym — status not_affected nie jest prawdą wieczną; powinien mieć justification, evidence, timestamp, owner, scope oraz expiry albo trigger do ponownej oceny.
Kto powinien naprawiać dependency transitive?
W przypadku libheif odpowiedź może wyglądać inaczej w każdej organizacji: (1) developer owns everything — zespół aplikacyjny dostaje „upgrade Next.js"; (2) platform team aktualizuje centralny template i wymusza rollout; (3) container owner — jeśli problem dotyczy native libraries z obrazu; (4) dependency service — centralny zespół zarządzający podatnościami zależności. Żaden model nie jest automatycznie poprawny; problemem jest brak jawnego ownership. Developer może odpowiedzieć „nie mamy libheif w package.json" i mieć rację — biblioteka pojawia się niżej. To klasyczny przypadek direct dependency ownership != transitive dependency ownership. Dobry system pokazuje ścieżkę (libheif ← sharp ← Next.js ← customer-portal), żeby developer rozumiał, skąd wziął się finding.
Remediation nie zawsze oznacza zmianę najniższej biblioteki. Developer aplikacji Next.js może nie kontrolować wersji libheif bezpośrednio; najbezpieczniejszą ścieżką bywa upgrade Next.js (vendor frameworka wykonał integrację i dobrał właściwe zachowanie), a doraźnym mitigation — wyłączenie AVIF. Trzy poziomy działania:
Nie są tym samym.
Jak powinien wyglądać ticket VM?
Zamiast „Critical vulnerability in Next.js — upgrade ASAP", lepszy ticket:
To ticket, który developer może wykonać.
Co powinien zrobić Vulnerability Engineer?
- Zidentyfikować wszystkie wersje Next.js — także internal tools, admin portals, preview/staging, developer platforms, ephemeral deployments.
- Zbudować listę affected versions dla obu advisory.
- Dodać runtime OS (kluczowe dla CVE-2026-75604).
- Zidentyfikować architekturę routerów (Pages, App, Cache Components).
- Sprawdzić Image Optimization.
- Sprawdzić AVIF (czy realnie przetwarzany).
- Sprawdzić attacker-controlled input (czy atakujący dostarczy obraz do pipeline).
- Ustalić dependency path (czy podatny libheif jest w deployed artifact).
- Zastosować mitigation, jeśli pełny upgrade nie może wejść od razu.
- Zaktualizować do patched release (15.5.24 / 16.3.3).
- Zweryfikować — nie zamykać na podstawie „developer says patched"; potrzebne build/deployment evidence, rescan, runtime verification.
Wersja jest początkiem analizy, nie końcem
Tradycyjne VM budowano wokół wygodnego modelu product + version → CVE i version < fixed_version => vulnerable. Ten model nadal jest potrzebny, ale dla nowoczesnych aplikacji za prosty. Potrzebujemy: component + version + dependency path + runtime + configuration + feature usage + reachability + exposure + attacker-controlled input. Dopiero wtedy możemy mówić o realnej exploitability. Sierpniowe dwa Critical RCE są dobrym przykładem, bo na dashboardzie wyglądają identycznie (Critical, RCE, Unauthenticated, Next.js, patch available), a pod spodem to dwie różne historie:
Jeśli dashboard VM nie potrafi pokazać tej różnicy, VE musi ją odtworzyć ręcznie.
Czy mimo wszystko po prostu patchować wszystko?
W wielu przypadkach — tak. Jeżeli poprawka jest dostępna, przetestowana i łatwa do wdrożenia, analiza exploitability nie powinna stać się wymówką „udowodnijcie najpierw, że da się nas zaatakować" — to antywzorzec. Celem analizy nie jest unikanie patchowania, tylko poprawne ustawienie kolejności, ograniczenie szumu, świadome mitigation, identyfikacja najbardziej zagrożonych deploymentów, właściwe ownership i wiarygodne zarządzanie wyjątkami. Vendor jasno zaleca upgrade, więc finalna rekomendacja pozostaje prosta: zaktualizować Next.js do 15.5.24 albo 16.3.3 zgodnie z używaną linią. Ale dojście do tej decyzji na poziomie setek aplikacji nie powinno polegać wyłącznie na czerwonej etykiecie.
A jeśli organizacja nie może teraz zaktualizować? Wtedy zaczyna się właściwy vulnerability engineering. Dla CVE-2026-75604 pytamy o Windows-hosting, wymagane elementy architektury, możliwość zmiany hostowania, ograniczenia exposure, przebudowy deploymentu. Dla AVIF: czy możemy wyłączyć AVIF/Image Optimization, ograniczyć źródła obrazów, czy attacker-controlled images mogą dotrzeć do pipeline, czy warstwa aplikacyjna/reverse proxy może blokować ryzykowne wejście. Każde mitigation musi później zostać zweryfikowane — nie wystarczy zapisać „mitigation implemented"; potrzebny jest dowód, że attack path faktycznie został przerwany. Compensating control nie powinien zamieniać się w wieczny exception — wyłączenie AVIF powinno mieć owner, date, evidence, verification i expiry / re-check condition (np. on_nextjs_upgrade: true, on_image_config_change: true). To już nie ticket management, tylko zarządzanie stanem bezpieczeństwa.
Dwa Critical RCE i jedna najważniejsza zasada
Finding z narzędzia jest informacją o możliwym problemie. Vulnerability Engineering ustala, co ten problem znaczy w konkretnym środowisku.
CVE-2026-75604 pokazuje, że exploitability może zależeć od OS + filesystem + architecture + configuration. Problem libheif/AVIF — że może zależeć od dependency chain + feature usage + input reachability. Oba są Critical, oba wymagają patchowania. Ale droga od „scanner says vulnerable" do „we understand the risk" jest zupełnie inna. Warto też pamiętać o granicach narzędzi: różne skanery konsumują inne feeds i aktualizują się w różnym czasie; SCA często nie ustali runtime OS ani użycia Pages/App Router; typowy version-based scanner nie stwierdzi, że AVIF jest wyłączony; SBOM mówi, co istnieje, nie zawsze co jest wykonywane; a VEX to sposób przekazania decyzji — ktoś nadal musi ją poprawnie podjąć i aktualizować.
Wnioski praktyczne
- Nie czekaj z aktualizacją tylko dlatego, że analizujesz exploitability.
- Zaktualizuj do 15.5.24 lub 16.3.3 zgodnie z linią release.
- Dla CVE-2026-75604 zinwentaryzuj szczególnie deploymenty Windows.
- Powiąż findings z runtime OS.
- Sprawdź architekturę routerów i cache.
- Dla GHSA-2xp9-vwfh-vxw4 zinwentaryzuj użycie Image Optimization i AVIF.
- Zidentyfikuj ścieżkę zależności do
libheif. - Jeśli patch nie może wejść natychmiast, wykorzystaj mitigation — zwłaszcza wyłączenie podatnej funkcji.
- Nie zamykaj nieeksploatowalnych deploymentów jako „false positive", jeśli komponent jest w affected range.
- Używaj statusu zależnego od kontekstu i zapisuj warunek ponownej oceny.
- Po wdrożeniu poprawki wykonaj rescan lub inną techniczną walidację.
- Zachowaj audit trail: co wiedzieliśmy, jaka była konfiguracja i dlaczego wybraliśmy taki priorytet.
Bo kilka miesięcy później największą wartością VM nie będzie „ticket closed", tylko odpowiedź na pytanie: dlaczego uznaliśmy, że ten konkretny system był — albo nie był — realnie podatny?
Źródła
- Next.js / Vercel — GHSA-p293-qw3h-jr36: Unauthenticated RCE on windows-hosted servers (CVE-2026-75604, 25 sierpnia 2026) — github.com/vercel/next.js
- Next.js / Vercel — GHSA-2xp9-vwfh-vxw4: Unauthenticated RCE in Image Optimization API when AVIF files are used (25 sierpnia 2026) — github.com/vercel/next.js
- libheif — GHSA-g89c-p67h-r497: Heap buffer overflow in scale_nearest_neighbor() (fixed: libheif 1.23.2, 25 sierpnia 2026) — github.com/strukturag/libheif
- Next.js — August 2026 Security Release (15.5.24 / 16.3.3, 25 sierpnia 2026) — nextjs.org/blog
Nota redakcyjna: artykuł opisuje stan publicznie dostępnych informacji na 28 sierpnia 2026, godz. 09:11 CEST. Security advisories, zakres affected versions, identyfikatory CVE/GHSA, dane o exploitability oraz upstream dependencies mogą być później aktualizowane. Przed decyzją operacyjną należy ponownie sprawdzić aktualne advisory producenta i stan konkretnego deploymentu.