ApostropheCMS: Client-side Prototype Pollution w apos.http.parseQuery prowadzące do DOM-based XSS przeciwko uwierzytelnionym użytkownikom
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: wersjaapostrophe@4.0.0zawiera już podatną logikę (parentKey = 'root') bez filtrowania__proto__/constructor/prototype, a wersjaapostrophe@4.32.2pozostaje pod tym względem niezmieniona. Podatna logikaparseQuery()została wprowadzona w commicie0255086bf(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ą kompilacjimodules/@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):
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:
- 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),contextprzyjmuje wartośćObject.prototype. - 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:
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):
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:
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):
- Zaloguj się jako dowolny użytkownik.
- Przejdź do:
https://TARGET/?__proto__[prop]=polluted - W konsoli przeglądarki wpisz:
Object.prototype.prop> zwraca"polluted".
B. DOM-based XSS:
- Zaloguj się jako dowolny użytkownik.
- Przejdź do:
https://TARGET/?__proto__[innerHTML]=%3Cimg/src/onerror%3dalert(1)%3E 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():
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
datai 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-coremountElementenumeracja właściwości (~linia 5741);@vue/runtime-dompatchDOMPropgałąź 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():
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
dataoraz kontenerów przy użyciuObject.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.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Paczka npm | apostrophe | < 4.33.0 | 4.33.0 |
Odnośniki
- https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-m2m5-rw3w-cwmj
- 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/apostrophe@4.33.0
- 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ć.
- 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
- 2020: 2
- 2021: 2
- 2022: 0
- 2023: 0
- 2024: 0
- 2025: 0
- 2026: 23
-
ApostropheCMS: Client-side Prototype Pollution w apos.http.parseQuery prowadzące do DOM-based XSS przeciwko uwierzytelnionym użytkownikom ten wpis
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.