Średnia

ApostropheCMS: Nagłówek Host kontroluje źródło pobierania obrazów w @apostrophecms/ai-helper, umożliwiając SSRF

  • Ujawniono: 20.10.2026
  • Aktualizacja: 21.10.2026 10:00

Podsumowanie

Pakiet @apostrophecms/ai-helper buduje adres URL do pobierania obrazu po stronie serwera poprzez dołączenie ścieżki względnej magazynu do req.baseUrl. Gdy witryna nie ma skonfigurowanego globalnego bazowego adresu URL (ustawienie domyślne), req.baseUrl jest pobierany z przychodzącego nagłówka HTTP Host. Uwierzytelniony edytor może wygenerować obraz AI w formacie base64 (który jest zapisywany bez zachowanego url), a następnie wysłać nieautoryzowane żądanie zatwierdzenia PATCH ze wskazanym przez siebie nagłówkiem Host. Serwer wysyła wówczas wychodzące żądanie GET do wybranego przez atakującego źródła (origin), co tworzy podatność Server-Side Request Forgery (SSRF) umożliwiającą dotarcie do hostów i portów normalnie niedostępnych dla klienta. Podatny kod znajduje się w packages/ai-helper/modules/@apostrophecms/ai-helper-image/index.js.

Przyczyna błędu

Ścieżka akceptacji obrazu traktuje req.baseUrl jako zaufane źródło przy tworzeniu wewnętrznego adresu docelowego, jednak req.baseUrl zależy od atakującego, o ile witryna nie posiada skonfigurowanego bazowego URL. Mechanizm budowania adresów URL łączy domyślną lokalną ścieżkę magazynu (która jest względna, np. /uploads) i przetwarza ją względem req.baseUrl; ponieważ lewy operand jest względny, ostateczne źródło jest w całości pobierane z req.baseUrl. W tym miejscu brakuje dwóch mechanizmów ochronnych obecnych gdzie indziej: ścieżka generowania weryfikuje uprawnienia do edycji, natomiast ścieżka akceptacji nie dokonuje żadnej autoryzacji ani weryfikacji właściciela, a rozwiązane źródło nie podlega walidacji ani sprawdzaniu z białą listą przed pobraniem.

packages/ai-helper/modules/@apostrophecms/ai-helper-image/index.js
  aiHelperImageUrl (line 91-92): image.url || new URL(self.uploadfs.getUrl() + `/ai-helper-images/${image._id}.png`, req.baseUrl)
  aiHelperFetchImage (line 64-65): fetch(self.aiHelperImageUrl(req, image))
  apiRoutes.patch['ai-helper/:_id'] (line 314-333): findOne({ _id }) only; no permission or ownership check
packages/apostrophe/modules/@apostrophecms/i18n/index.js
  setPrefixUrls (line 799-820): req.baseUrl falls back to `${req.protocol}://${host}`, host from the Host header when apos.baseUrl is unset

Wpływ

Atakujący posiadający nisko uprzywilejowane konto edytora (uprawnienie wymagane do wygenerowania obrazu AI, co tworzy wpis base64 bez zapisanego url i zwraca jego identyfikator) może wymusić na serwerze wysłanie wychodzących żądań GET do wybranego przez siebie źródła poprzez spreparowany nagłówek Host w żądaniu akceptacji. Prowadzi to do podatności SSRF: badania wewnętrznych hostów i portów niedostępnych z pozycji sieciowej atakującego oraz ograniczonej częściowo ślepej eksfiltracji, w której odpowiedź możliwa do przetworzenia jako obraz pod stałą ścieżką staje się widocznym załącznikiem. Ścieżka żądania jest na stałe ustawiona na /uploads/ai-helper-images/<id>.png, więc pod kontrolą atakującego znajduje się jedynie źródło (host, port i protokół http); pobieranie dowolnych ścieżek, takich jak metadane chmurowe, nie jest możliwe. Nie wykazano naruszenia integralności ani dostępności. Wykorzystanie podatności wymaga domyślnego wdrożenia, w którym nie skonfigurowano globalnego bazowego URL, stosowany jest lokalny magazyn plików oraz włączony jest dostawca zwracający dane obrazu w formacie base64.

Proof of Concept

Preconditions:
  - ApostropheCMS site with @apostrophecms/ai-helper installed and a configured
    base64-returning image provider.
  - No global base URL configured (apos.baseUrl unset), default local uploadfs.
  - Attacker holds an account with edit capability on at least one content type.
  1. Uwierzytelnij się jako edytor i wygeneruj obraz AI w formacie base64: POST /api/v1/@apostrophecms/image/ai-helper z treścią {"prompt":"x"}. Odpowiedź zawiera nowy identyfikator rekordu <_id>. Zapisany rekord nie posiada pola url.
  2. Wyślij żądanie akceptacji ze złośliwym nagłówkiem Host:
PATCH /api/v1/@apostrophecms/image/ai-helper/<_id> HTTP/1.1
Host: 169.254.169.254
Content-Type: application/json

{"accepted": true}
  1. Serwer ustala req.baseUrl jako http://169.254.169.254, buduje cel pobierania http://169.254.169.254/uploads/ai-helper-images/<_id>.png i wysyła wychodzące żądanie.
Derivation confirmed against source logic:
  base64 record (no url) + Host: 169.254.169.254
    -> new URL("/uploads/ai-helper-images/<_id>.png", "http://169.254.169.254")
    -> server fetches: http://169.254.169.254/uploads/ai-helper-images/<_id>.png
  control (apos.baseUrl set to https://cms.example.com):
    -> fetch target uses https://cms.example.com (Host ignored)
  control (record stores its own url):
    -> aiHelperImageUrl short-circuits to the stored url (no SSRF)

Łańcuch ataku

  1. Ekspozycja: Moduł @apostrophecms/ai-helper-image rejestruje trasy POST i PATCH pod /api/v1/@apostrophecms/image/ai-helper, dostępne dla każdej uwierzytelnionej sesji w serwisie z zainstalowanym pakietem.
  2. Kontrola: Atakujący kontroluje nagłówek HTTP Host w żądaniu PATCH; globalne oprogramowanie pośredniczące locale kopiuje go do req.baseUrl, ponieważ apos.baseUrl nie jest ustawione (packages/apostrophe/modules/@apostrophecms/i18n/index.js:805,816-819).
  3. Ścieżka: Atakujący generuje najpierw obraz base64 przez POST, co wstawia rekord bez url (ai-helper-image/index.js:282-284); zwrócony identyfikator jest następnie używany w żądaniu akceptacji PATCH, które wywołuje aiHelperFetchImage (:332).
  4. Ochrona: Procedura PATCH sprawdza jedynie req.body.accepted oraz istnienie rekordu przez findOne({ _id }) (:318,:322); nie weryfikuje uprawnień ani powiązania z właścicielem, w przeciwieństwie do trasy POST wywołującej checkPermissions (:176). Brak jest walidacji docelowego źródła przed pobraniem.
  5. Mechanizm: aiHelperImageUrl łączy względną ścieżkę z adresem pochodzącym z nagłówka Host req.baseUrl, a aiHelperFetchImage wywołuje fetch na uzyskanym wyniku (:91-92,:64-65).
  6. Rezultat: Serwer wykonuje wychodzące żądanie GET pod wskazany przez atakującego adres na stałej ścieżce, umożliwiając dostęp do wewnętrznych hostów i portów oraz ograniczone pobieranie odpowiedzi.

Analiza obejść zabezpieczeń

Zabezpieczenie checkPermissions (ai-helper-image/index.js:176) z trasy generowania nie występuje na trasie akceptacji, więc pobieranie nie jest chronione autoryzacją. Przetestowano i wykluczono następujące mechanizmy:

  • Domyślny lokalny magazyn zwraca ścieżkę względną: uploadfs.getUrl() zwraca /uploads, ponieważ lokalny backend tworzy uploadsUrl = (apos.baseUrl || '') + prefix + '/uploads', przez co new URL(relative, req.baseUrl) czerpie źródło z req.baseUrl. Skonfigurowanie bazowego URL lub bezwzględnego URL magazynu (S3/CDN) zapobiega atakowi, dlatego są to środki zaradcze, a nie konfiguracja domyślna.
  • Rekord base64 nie posiada zapisanego url: insertOne(image) zapisuje rekord, zanim image.url zostanie przypisane w obiekcie odpowiedzi w pamięci (:282-284 vs :294), więc moduł obsługi PATCH odczytuje rekord bez url, a warunek w aiHelperImageUrl nie zostaje spełniony.
  • Własność i enumeracja: GET oraz DELETE w tych rekordach mają zakres userId, a identyfikatory cuid2 są niezgadywalne, stąd realnym aktorem jest tworzący je edytor; ogranicza to wymagane uprawnienia do uwierzytelnionego użytkownika z niskimi prawami.
  • To nie jest duplikat: poprawka podatności SSRF dot. pretty-URL w rdzeniu dotyczy wyłącznie modułu @apostrophecms/file i nie modyfikuje packages/ai-helper; luka SSRF w imporcie rich-text używa innej trasy z białą listą hostów. Nie było wcześniejszego zgłoszenia dla @apostrophecms/ai-helper. Jest to odrębna, niezabezpieczona ścieżka wykonania.

Dotknięte wersje

  • Ecosystem: npm
  • Package: @apostrophecms/ai-helper
  • Confirmed affected range: >= 1.0.0-beta.10, <= 1.0.0-beta.12
  • Latest release checked: 1.0.0-beta.12 (npm registry, dist-tag latest)
  • Fix status: not fixed

Podatny wzorzec wprowadzono w 1.0.0-beta.10: wcześniejsze wydania aż do 1.0.0-beta.9 pobierały zapisany URL dostawcy bezpośrednio (fetch(url)) bez przetwarzania req.baseUrl i nie są podatne. Weryfikacja paczek 1.0.0-beta.10, 1.0.0-beta.11 i 1.0.0-beta.12 potwierdza obecność pełnego łańcucha podatności. Pakiet jest publikowany wyłącznie w kanale wersji przedpremierowych (beta).

Sugerowane rozwiązanie

Nie należy traktować req.baseUrl jako zaufanego źródła dla pobierania zasobów przez serwer. Należy wdrożyć na trasie akceptacji taką samą autoryzację, jak na trasie generowania, oraz dodać weryfikację właściciela rekordu. Niezależnie od tego należy ograniczyć cel pobierania: rozwiązywać względną ścieżkę względem zaufanej, skonfigurowanej na serwerze bazy zamiast pobieranej z żądania req.baseUrl, a także weryfikować lub filtrować białą listą docelowe źródło przed wywołaniem fetch. Zabezpieczenie powinno objąć zarówno format base64, jak i zapisany URL oraz wywołania aiHelperImageUrl i aiHelperFetchImage. Jako tymczasowe obejście bez zmian w kodzie można skonfigurować globalny bazowy URL oraz wprowadzić reguły sieciowe (egress) blokujące ruch wychodzący z serwera do wrażliwych hostów wewnętrznych i punktów końcowych metadanych.

Zgłoszone przez zx (GitHub: @manus-pi).

Co trzeba zrobić

Zaktualizuj pakiet do wersji 1.0.0-beta.13 lub nowszej. Jako obejście tymczasowe skonfiguruj globalny bazowy adres URL i zablokuj ruch wychodzący z serwera do sieci wewnętrznej.

Czego dotyczy

Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.

PlatformaPaczkaPodatne wersjePoprawka w wersji
Paczka npm @apostrophecms/ai-helper >= 1.0.0-beta.10, < 1.0.0-beta.13 1.0.0-beta.13

Odnośniki

Ocena CVSS 4.3/10
Wektor CVSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Słabość (CWE)
CWE-918
Platforma
npm
Paczka
@apostrophecms/ai-helper
Ujawniono
20.10.2026
Aktualizacja
21.10.2026 10:00

Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.

OSV / GitHub Advisory Database - GHSA-527g-ff96-x6wj - CC BY 4.0

Historia podatności: @apostrophecms/ai-helper

Wszystkie opisane podatności tej paczki (Paczka npm), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.

To jedyna znana podatność tej paczki.

Dowiedz się, które podatności dotyczą Twojej aplikacji

Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.

Podłącz swoją aplikację
Newsletter DockRay

Bezpieczeństwo aplikacji w Twojej skrzynce

Najważniejsze podatności, konkretne zalecenia i praktyczne wskazówki dla Twoich aplikacji. Wybierz technologie i śledź to, co ma znaczenie dla Twojego projektu.

  • Codzienne zestawienie nowych podatności, także krytycznych.
  • Powiadomienia o ważnych aktualizacjach.
  • Praktyczne porady o bezpieczeństwie i monitoringu aplikacji.

Pozostaw puste, aby otrzymywać informacje o wszystkich technologiach.

Zapis jest bezpłatny. Wypiszesz się jednym kliknięciem w wiadomości.