ApostropheCMS: Nagłówek Host kontroluje źródło pobierania obrazów w @apostrophecms/ai-helper, umożliwiając SSRF
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.
- Uwierzytelnij się jako edytor i wygeneruj obraz AI w formacie base64:
POST /api/v1/@apostrophecms/image/ai-helperz treścią{"prompt":"x"}. Odpowiedź zawiera nowy identyfikator rekordu<_id>. Zapisany rekord nie posiada polaurl. - 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}
- Serwer ustala
req.baseUrljakohttp://169.254.169.254, buduje cel pobieraniahttp://169.254.169.254/uploads/ai-helper-images/<_id>.pngi 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
- Ekspozycja: Moduł
@apostrophecms/ai-helper-imagerejestruje trasyPOSTiPATCHpod/api/v1/@apostrophecms/image/ai-helper, dostępne dla każdej uwierzytelnionej sesji w serwisie z zainstalowanym pakietem. - Kontrola: Atakujący kontroluje nagłówek HTTP
Hostw żądaniuPATCH; globalne oprogramowanie pośredniczącelocalekopiuje go doreq.baseUrl, ponieważapos.baseUrlnie jest ustawione (packages/apostrophe/modules/@apostrophecms/i18n/index.js:805,816-819). - Ścieżka: Atakujący generuje najpierw obraz base64 przez
POST, co wstawia rekord bezurl(ai-helper-image/index.js:282-284); zwrócony identyfikator jest następnie używany w żądaniu akceptacjiPATCH, które wywołujeaiHelperFetchImage(:332). - Ochrona: Procedura
PATCHsprawdza jedyniereq.body.acceptedoraz istnienie rekordu przezfindOne({ _id })(:318,:322); nie weryfikuje uprawnień ani powiązania z właścicielem, w przeciwieństwie do trasyPOSTwywołującejcheckPermissions(:176). Brak jest walidacji docelowego źródła przed pobraniem. - Mechanizm:
aiHelperImageUrlłączy względną ścieżkę z adresem pochodzącym z nagłówka Hostreq.baseUrl, aaiHelperFetchImagewywołujefetchna uzyskanym wyniku (:91-92,:64-65). - Rezultat: Serwer wykonuje wychodzące żądanie
GETpod 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 tworzyuploadsUrl = (apos.baseUrl || '') + prefix + '/uploads', przez conew URL(relative, req.baseUrl)czerpie źródło zreq.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, zanimimage.urlzostanie przypisane w obiekcie odpowiedzi w pamięci (:282-284vs:294), więc moduł obsługiPATCHodczytuje rekord bezurl, a warunek waiHelperImageUrlnie zostaje spełniony. - Własność i enumeracja:
GETorazDELETEw tych rekordach mają zakresuserId, 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/filei nie modyfikujepackages/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: npmPackage: @apostrophecms/ai-helperConfirmed affected range: >= 1.0.0-beta.10, <= 1.0.0-beta.12Latest 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.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Paczka npm | @apostrophecms/ai-helper | >= 1.0.0-beta.10, < 1.0.0-beta.13 | 1.0.0-beta.13 |
Odnośniki
- https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-527g-ff96-x6wj
- https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce
- https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350
- https://github.com/apostrophecms/apostrophe
- https://github.com/apostrophecms/apostrophe/releases/tag/@apostrophecms/ai-helper@1.0.0-beta.13
- 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.