Średnia

ApostropheCMS: Client-side Prototype Pollution w apos.http.parseQuery prowadzące do DOM-based XSS przeciwko uwierzytelnionym użytkownikom

  • Ujawniono: 20.10.2026
  • Aktualizacja: 20.10.2026 21:34

Podsumowanie

Funkcja pomocnicza po stronie klienta apos.http.parseQuery() w ApostropheCMS buduje zagnieżdżony obiekt na podstawie ciągu zapytania URL (query string) bez sanityzacji kluczy __proto__, constructor ani prototype. Przetworzenie ciągu zapytania kontrolowanego przez atakującego powoduje zapisanie dowolnych właściwości do Object.prototype (client-side prototype pollution).

Interfejs administracyjny ApostropheCMS wywołuje ten parser z parametrem window.location.search przy każdym załadowaniu uwierzytelnionej strony (inicjalizator data() paska kontekstu administratora). Ponieważ panel administracyjny bazuje na Vue, zanieczyszczona, wyliczalna właściwość na obiekcie Object.prototype jest następnie pobierana przez standardową procedurę montowania elementów Vue i zapisywana do drzewa DOM jako właściwość elementu, przekształcając zanieczyszczenie prototypu w atak DOM-based XSS w uwierzytelnionej sesji ofiary. Gadżet eskalacji jest powszechnie znanym, ogólnym zachowaniem biblioteki Vue; nową, podlegającą zgłoszeniu wadą jest brak sanityzacji źródła parseQuery wraz z uwierzytelnionym wyzwalaczem – oba elementy znajdują się w kodzie ApostropheCMS.

Każdy użytkownik, który nakłoni dowolnego zalogowanego użytkownika (od roli o najniższych uprawnieniach guest do roli o najwyższych uprawnieniach admin) do otwarcia spreparowanego adresu URL, może wykonać dowolny kod JavaScript w jego sesji.

Podatność ta jest inna niż CVE-2026-53609 (server-side prototype pollution poprzez apos.util.set() / $pullAll, naprawione w 4.31.0): dotyczy innej strony (klient a serwer), innej ścieżki w kodzie, a podatny kod w tym przypadku występuje w bieżącej wersji.

Pakiet

  • Ekosystem: npm
  • Pakiet: apostrophe
  • Wersje dotknięte podatnością: wszystkie wydania 4.x, od 4.0.0 do 4.32.2 (obecnie latest). Potwierdzone poprzez inspekcję opublikowanych archiwów npm: wersja apostrophe@4.0.0 zawiera już podatną logikę (parentKey = 'root') bez filtrowania __proto__/constructor/prototype, a wersja apostrophe@4.32.2 pozostaje pod tym względem niezmieniona. Podatna logika parseQuery() została wprowadzona w commicie 0255086bf (2020-11-12), na długo przed wersją 4.0, dlatego ApostropheCMS 3.x również jest dotknięty problemem (ten sam kod znajduje się w 3.x pod starszą ścieżką kompilacji modules/@apostrophecms/util/ui/public/2-http.js).
  • Wersje załatane: brak w momencie publikacji raportu.

Szczegóły

Problem składa się z trzech części. Części 1 i 2 znajdują się w kodzie ApostropheCMS i stanowią zgłaszaną podatność. Część 3 to gadżet eskalacji będący standardowym zachowaniem Vue, ujęty w celu zapewnienia kompletności opisu.

1. Prymityw zanieczyszczenia: źródło (ApostropheCMS)

modules/@apostrophecms/util/ui/src/http.js, funkcja apos.http.parseQuery() (linia ~301) oraz jej wewnętrzna funkcja patch() (linie ~317–354):

js
function patch(key, value) {
  let match;
  let parentKey = 'root';
  let context = data;                    // data = {}  (ordinary object)
  key = decodeURIComponent(key);
  const path = key.split('[');
  path.forEach(function(subKey) {
    if (subKey === ']') {
      /* array handling */
    } else if (subKey.match(/^\d+]/)) {
      /* numeric index handling */
    } else {
      match = subKey.replace(']', '');
      if (!context[parentKey]) {         // (A) truthy check, not an own-property check
        context[parentKey] = {};
      }
      context = context[parentKey];      // (B) descends into whatever this resolves to
      parentKey = match;
    }
  });
  value = (value === null) ? value : decodeURIComponent(value);
  if (Array.isArray(context[parentKey])) {
    context[parentKey].push(value);
  } else if (context[parentKey] !== undefined) {
    context[parentKey] = [ context[parentKey], value ];
  } else {
    context[parentKey] = value;          // (C) assignment onto the resolved object
  }
}

Żaden z kluczy __proto__, constructor ani prototype nie jest odrzucany. Przetwarzanie wejścia __proto__[x]=v:

  1. Segment __proto__: w punkcie (A), context['__proto__'] wskazuje na odziedziczony prototyp obiektu (wartość typu truthy), w związku z czym inicjalizator {} zostaje pominięty; w punkcie (B), context przyjmuje wartość Object.prototype.
  2. Segment x: w punkcie (C), Object.prototype['x'] = v.

Warunek sprawdzający wartość pod kątem truthy w punkcie (A) (zamiast sprawdzenia przynależności właściwości do obiektu – own-property) umożliwia przejście do odziedziczonego prototypu. Ponieważ przypisanie w punkcie (C) jest zwykłym zapisem właściwości, powstała właściwość Object.prototype jest wyliczalna (enumerable), co ma kluczowe znaczenie dla działania gadżetu (3).

2. Wyzwalacz: osiągany przy każdym załadowaniu uwierzytelnionej strony (ApostropheCMS)

modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue. Inicjalizator data() komponentu (linia ~64) wykonuje się, gdy montowany jest pasek kontekstu administratora (tj. przy każdym załadowaniu uwierzytelnionej strony) i przekazuje surowy ciąg zapytania bezpośrednio do podatnego parsera:

js
data() {
  const query = apos.http.parseQuery(location.search);   // <- attacker-controlled -> (1) pollution, on mount
  const draftMode = query.aposMode || 'published';
  // ...
}

Metoda refresh() tego samego komponentu (linia ~606) przekazuje window.location.search do parseQuery() ponownie podczas zdarzeń odświeżania paska administratora oraz dołącza go do doc._url (ponownie parsowanego w apos.http.remote()), zapewniając dodatkowe punkty wyzwolenia.

Pasek kontekstu administratora jest montowany wyłącznie wtedy, gdy sesja jest aktywna i ładowany jest pakiet administracyjny, więc żaden z tych fragmentów kodu nie wykonuje się dla użytkowników anonimowych. Z tego powodu zanieczyszczenie prototypu występuje tylko po zalogowaniu. Całość stanowi kod bazowy ApostropheCMS (node_modules/apostrophe/...) i nie pochodzi z projektów zewnętrznych ani szablonów startowych.

3. Gadżet eskalacji: montowanie elementu Vue (zachowanie frameworka; znane, nienowe)

Zademonstrowany ładunek zanieczyszcza właściwość innerHTML (__proto__[innerHTML]=<img/src/onerror=alert(1)>). Ujście (sink), które z niej korzysta, to standardowa ścieżka montowania elementów w bibliotece Vue, a nie kod ApostropheCMS. Potwierdzone w dołączonym frameworku (vue / @vue/runtime-core / @vue/runtime-dom, wszystkie wersje 3.5.42):

@vue/runtime-core mountElement, enumeracja właściwości (linia ~5741):

js
if (props) {
  for (const key in props) {                              // for...in walks the prototype chain
    if (key !== "value" && !shared.isReservedProp(key)) { // isReservedProp does NOT list innerHTML
      hostPatchProp(el, key, null, props[key], ...);      // props["innerHTML"] → inherited polluted value
    }
  }

@vue/runtime-dom patchDOMProp (linia ~583), osiągane poprzez hostPatchProp:

js
function patchDOMProp(el, key, value, ...) {
  if (key === "innerHTML" || key === "textContent") {
    if (value != null) {
      el[key] = key === "innerHTML" ? unsafeToTrustedHTML(value) : value;  // el.innerHTML = payload
    }
    return;
  }

Mechanizm: za każdym razem, gdy Vue montuje element posiadający niepusty obiekt props bez własnej właściwości innerHTML (standard w trakcie renderowania panelu administratora), for (const key in props) wylicza odziedziczoną, wyliczalną właściwość innerHTML z Object.prototype; przechodzi ona przez filtry "value" oraz isReservedProp; props["innerHTML"] ewaluuje się do ładunku atakującego; a patchDOMProp zapisuje go do el.innerHTML. unsafeToTrustedHTML to wrapper Trusted-Types w Vue, nie filtr sanityzujący; przekazuje dane bezpośrednio, chyba że wymuszone jest użycie Trusted Types (co nie jest ustawieniem domyślnym).

Gadżet ten jest dobrze udokumentowaną i powszechną metodą eskalacji z client-side PP do XSS w aplikacjach Vue. Nie jest zgłaszany jako nowo odkryta podatność i nie leży w gestii poprawki ApostropheCMS, ponieważ nie można go usunąć bez modyfikacji Vue. Jego znaczenie polega na tym, że umożliwia niezawodne wykorzystanie podatności zanieczyszczenia prototypu (1) w dowolnej sesji administratora ApostropheCMS.

Proof of Concept

Wymagania wstępne: działająca instancja ApostropheCMS oraz dowolna uwierzytelniona sesja (wystarczy konto z rolą guest). Tester lub ofiara musi być zalogowana.

A. Prototype pollution (weryfikacja w konsoli):

  1. Zaloguj się jako dowolny użytkownik.
  2. Przejdź do: https://TARGET/?__proto__[prop]=polluted
  3. W konsoli przeglądarki wpisz: Object.prototype.prop > zwraca "polluted".

B. DOM-based XSS:

  1. Zaloguj się jako dowolny użytkownik.
  2. Przejdź do: https://TARGET/?__proto__[innerHTML]=%3Cimg/src/onerror%3dalert(1)%3E
  3. alert(1) wykona się w kontekście sesji uwierzytelnionej ofiary.

Aby przechwycić stos wywołań w celach dowodowych: DevTools > Sources > otwórz bundle administratora > pretty-print > wyszukaj unsafeToTrustedHTML (lub === "innerHTML") > ustaw breakpoint w gałęzi innerHTML w patchDOMProp > odśwież URL ładunku przy zalogowanym użytkowniku > sprawdź value i prześledź stos wywołań przez hostPatchProp do for…in w mountElement.

Zarówno wariant A, jak i B zostały odtworzone na wersji ApostropheCMS 4.32.2 (Vue 3.5.42) w przeglądarkach Chrome, Firefox oraz Edge.

Wpływ

Każda strona, która zdoła nakłonić uwierzytelnionego użytkownika ApostropheCMS do otwarcia spreparowanego adresu URL, może wykonać dowolny kod JavaScript w jego sesji. Ponieważ obecność gadżetu eskalacji wynika z natywnego zachowania biblioteki Vue, exploit działa w sposób powtarzalny. Skutki zależą od uprawnień ofiary:

  • Dowolna uwierzytelniona ofiara: możliwość odczytu danych i wykonywania akcji dostępnych dla danego konta; kradzież ciasteczka sesyjnego / tokenu CSRF z uwierzytelnionego kontekstu.
  • Ofiara z rolą redaktora/administratora: wstrzyknięty skrypt może wykonywać zapytania do uwierzytelnionych interfejsów REST API panelu zarządzania, tworzyć lub modyfikować dokumenty oraz konta użytkowników, zmieniać konfigurację, co może prowadzić do przejęcia konta i pełnego skompromitowania instancji CMS.

Zanieczyszczenie prototypu może również wpływać na inne, niepowiązane ścieżki kodu odczytujące dziedziczone właściwości, w związku z czym mogą istnieć także inne gadżety poza ujściem innerHTML w Vue.

Mitygacja

Poprawka powinna zostać zaimplementowana w ApostropheCMS, w miejscu źródła (1). Gadżet (3) wynika z zachowania frameworka i nie można oczekiwać jego modyfikacji, dlatego prawidłowym rozwiązaniem jest sanityzacja parseQuery.

Podstawowa poprawka: odrzucanie kluczy modyfikujących prototyp w parseQuery/patch():

js
apos.http.parseQuery = function(query) {
  query = query.replace(/^\?/, '');
  const data = {};
  const DANGEROUS = new Set([ '__proto__', 'constructor', 'prototype' ]);
  query.split('&').forEach(function(pair) {
    // ... unchanged ...
  });
  return data.root || {};

  function patch(key, value) {
    let match;
    let parentKey = 'root';
    let context = data;
    key = decodeURIComponent(key);
    const path = key.split('[');
    let dangerous = false;
    path.forEach(function(subKey) {
      const cleanKey = subKey.replace(']', '');
      if (DANGEROUS.has(cleanKey)) {   // refuse to traverse into prototype-affecting keys
        dangerous = true;
        return;
      }
      // ... existing subKey handling ...
    });
    if (dangerous) {
      return;                          // discard this parameter entirely
    }
    // ... existing assignment ...
  }
};

Obrona w głąb (Defense in depth):

  • Zastąpienie warunku sprawdzającego truthy w punkcie (A) weryfikacją właściwości własnej: if (!Object.prototype.hasOwnProperty.call(context, parentKey)), aby żaden klucz nie mógł przejść do odziedziczonego prototypu.
  • Tworzenie obiektu data i wszelkich kontenerów pomocniczych za pomocą Object.create(null), tak aby obiekty pośrednie nie posiadały łańcucha prototypów.

Tymczasowe środki zaradcze dla administratorów: ograniczenie liczby kont z dostępem uwierzytelnionym oraz zachowanie ostrożności przy otwieraniu niezaufanych odnośników podczas aktywnej sesji logowania. Po stronie serwera nie ma przełącznika konfiguracyjnego umożliwiającego wyłączenie podatnego zachowania po stronie klienta.

Odnośniki

  • Źródło (ApostropheCMS): apostrophe/modules/@apostrophecms/util/ui/src/http.js — parseQuery() / patch()
  • Wyzwalacz (ApostropheCMS): apostrophe/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue — data() (~linia 64), refresh() (~linia 606)
  • Gadżet eskalacji (Vue 3.5.42, zachowanie frameworka): @vue/runtime-core mountElement enumeracja właściwości (~linia 5741); @vue/runtime-dom patchDOMProp gałąź innerHTML (~linia 583)
  • Powiązane, lecz odrębne zagadnienie: CVE-2026-53609 / GHSA-6h5j-32cf-4253 (server-side prototype pollution poprzez apos.util.set() / $pullAll, naprawione w 4.31.0)
  • CWE-1321; CWE-79

Podziękowania

Nazwa użytkownika GitHub: Xx-LoGiCaL-xX

Co trzeba zrobić

Poprawka powinna zostać wdrożona w ApostropheCMS na poziomie źródła (1) poprzez sanityzację parseQuery.

Główna poprawka: odrzucenie kluczy wpływających na prototyp w parseQuery/patch():

js
apos.http.parseQuery = function(query) {
  query = query.replace(/^\?/, '');
  const data = {};
  const DANGEROUS = new Set([ '__proto__', 'constructor', 'prototype' ]);
  query.split('&').forEach(function(pair) {
    // ... unchanged ...
  });
  return data.root || {};

  function patch(key, value) {
    let match;
    let parentKey = 'root';
    let context = data;
    key = decodeURIComponent(key);
    const path = key.split('[');
    let dangerous = false;
    path.forEach(function(subKey) {
      const cleanKey = subKey.replace(']', '');
      if (DANGEROUS.has(cleanKey)) {   // refuse to traverse into prototype-affecting keys
        dangerous = true;
        return;
      }
      // ... existing subKey handling ...
    });
    if (dangerous) {
      return;                          // discard this parameter entirely
    }
    // ... existing assignment ...
  }
};

Środki ochrony w głąb:

  • Zastąpienie sprawdzenia typu truthy w punkcie (A) sprawdzeniem własności: if (!Object.prototype.hasOwnProperty.call(context, parentKey)).
  • Tworzenie obiektu data oraz kontenerów przy użyciu Object.create(null).

Tymczasowe zalecenia: Ograniczenie uprawnień do kont uwierzytelnionych oraz unikanie otwierania niezaufanych łączy w trakcie trwania zalogowanej sesji. Brak serwerowej opcji konfiguracyjnej wyłączającej podatne zachowanie klienta.

Czego dotyczy

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

PlatformaPaczkaPodatne wersjePoprawka w wersji
Paczka npm apostrophe < 4.33.0 4.33.0

Odnośniki

Ocena CVSS 6.1/10
Wektor CVSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Słabość (CWE)
CWE-1321, CWE-79
Platforma
npm
Paczka
apostrophe
Ujawniono
20.10.2026
Aktualizacja
20.10.2026 21:34

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

OSV / GitHub Advisory Database - GHSA-m2m5-rw3w-cwmj - CC BY 4.0

Historia podatności: apostrophe

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

Wszystkie (27) w archiwum
Wszystkich wpisów
27
Pierwsza → ostatnia
09.2020 10.2026
Bez poprawki
4
Wykorzystywana w atakach
0

Rozkład istotności

  • Krytyczna 2
  • Wysoka 8
  • Średnia 13
  • Niska 4

Ujawnienia w latach

  1. 2020: 2
  2. 2021: 2
  3. 2022: 0
  4. 2023: 0
  5. 2024: 0
  6. 2025: 0
  7. 2026: 23
Istotność:
  1. ApostropheCMS: Client-side Prototype Pollution w apos.http.parseQuery prowadzące do DOM-based XSS przeciwko uwierzytelnionym użytkownikom ten wpis
    Średnia · 6.1 CVE-2026-113659 < 4.33.0 4.33.0
  2. Wysoka · 7.3 CVE-2026-45011 = 4.29.0 brak poprawki
  3. Krytyczna · 9.8 CVE-2021-25979 >= 2.63.0, < 3.4.0 3.4.0

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.