У Livewire PowerGrid знайшли вразливість GHSA-wj8p-8c55-266p: колонка з searchableRaw() або searchableJson() додає свою умову пошуку так, що вона обходить усі обмеження, які таблиця задає в datasource(). Автентифікований користувач, якому доступне поле пошуку, бачить рядки, яких бачити не повинен, наприклад записи інших користувачів. Критичність середня (CVSS 4.3, CWE-863 - неправильна авторизація), повідомив про проблему pboese.
Хто уражений і як оновитися
Уражені всі версії до 7.0.0 включно. Автор звіту перевірив 6.11.1 і 7.0.0, а старіші версії з searchableRaw() найімовірніше теж уражені. Під загрозою таблиця, у якої одночасно:
- обмеження рядків прописане в
datasource() (поточний користувач, статус, вкладка, категорія);
- хоча б одна колонка оголошена через
searchableRaw() або searchableJson().
Виправлення вийшло 31 жовтня в PowerGrid 7.0.1. Сам фікс живе в пакеті power-components/turbine, на якому побудовано PowerGrid 7: версія 0.14.1 застосовує сирі orWhere-умови всередині групи пошуку, а PowerGrid 7.0.1 тепер вимагає turbine ^0.14.1. Перевірте, що стоїть у проєкті, й оновіться разом із залежностями:
composer show power-components/livewire-powergrid power-components/turbine
composer update power-components/livewire-powergrid --with-dependencies
Із гілкою 6.x гірше: остання її версія 6.11.1 уражена, а патча для неї немає. Перехід на 7.x - це окрема робота (PHP 8.3+, Laravel 12+, Livewire 4, Tailwind CSS 4), тож до нього варто прибрати searchableRaw() і searchableJson() з таблиць, які обмежують рядки в datasource().
Як ламається запит
Звичайні колонки з searchable() PowerGrid загортає в одну групу where(function ...). А searchableRaw() зберігає свою умову з методом orWhereRaw, і окремий крок конвеєра застосовує її вже після групи пошуку, прямо до кореневого запиту. В advisory наведено SQL, який виходить у підсумку:
where `orders`.`user_id` = ? and (`orders`.`number` LIKE ? or ...) or customers.name LIKE ?
AND у SQL зв'язує сильніше за OR, тож база читає це так:
(`orders`.`user_id` = ? and (...)) or customers.name LIKE ?
Друга половина не знає нічого про user_id. Варто ввести в пошук "acme", і таблиця покаже замовлення всіх користувачів, чий клієнт зветься Acme. Експорт будує набір даних тими самими кроками, тож вивантажить ті самі чужі рядки у файл.
Зверніть увагу, що для relationSearch() у 7.x цю пастку вже обійшли: умова пошуку по зв'язку стоїть усередині групи, і коментар у коді описує саме цей ризик. searchableRaw() просто не отримав того самого лікування - до 7.0.1.
Чому глобальні скопи вистояли
Якщо обмеження на "свої" рядки зроблено глобальним скопом (Global Scope) моделі, вразливість його не зачіпає. Eloquent застосовує глобальні скопи наприкінці, перед виконанням запиту, і коли серед уже наявних умов є OR, сам бере їх у дужки, а умову скопу додає через AND окремо:
where ((`orders`.`number` LIKE ? or ...) or customers.name LIKE ?) and `orders`.`user_id` = ?
Умови, написані прямо в datasource(), такого захисту не мають: для Eloquent це звичайні where того самого рівня, що й рядок orWhereRaw, доданий пізніше. А саме в datasource() найчастіше й обмежують таблицю поточним користувачем.
Та сама пастка у власному коді
PowerGrid тут лише приклад. orWhere без групування - одна з найпоширеніших помилок в Eloquent, і вона так само тихо ламає фільтр доступу в контролерах, API та звітах. Кілька звичок, які її прибирають:
- Будь-який
orWhere - тільки всередині групи. Пошук по кількох полях загортайте в замикання або беріть whereAny(), який групує умови сам:
Order::query()
->where('user_id', $request->user()->id)
->where(function ($query) use ($term) {
$query->where('number', 'like', "%{$term}%")
->orWhere('note', 'like', "%{$term}%");
});
// або
Order::query()
->where('user_id', $request->user()->id)
->whereAny(['number', 'note'], 'like', "%{$term}%");
-
Дивіться на SQL. toSql() або toRawSql() у тесті чи в tinker одразу покаже, де стоять дужки. Саме так перевіряє виправлення новий тест у Turbine: він очікує в запиті "in_stock" = ? and (dishes.name like ?).
-
Тестуйте поведінку, а не лише SQL. Найнадійніший тест на такі речі - два користувачі зі схожими даними і перевірка, що пошук одного не віддає записів іншого:
it('does not leak other users orders through search', function () {
$alice = User::factory()->create();
$bob = User::factory()->create();
Order::factory()->for($bob)->create(['number' => 'ACME-2']);
$this->actingAs($alice)
->get('/orders?search=ACME')
->assertOk()
->assertDontSee('ACME-2');
});
Такий тест ловить проблему незалежно від того, де вона сидить: у вашому запиті, у пакеті таблиць чи в макросі, який хтось додав рік тому.
Що зробити зараз
Якщо в проєкті є PowerGrid, пройдіться пошуком по searchableRaw( і searchableJson(. Немає збігів - вразливість вас не стосується, але оновитися до 7.0.1 все одно варто. Є збіги в таблицях, що обмежують рядки в datasource(), - оновлюйтеся на 7.x негайно або, на 6.x, приберіть ці колонки з пошуку до переходу. Для гілки 6.x це не перша проблема за рік: у липні в ній уже закривали SQL-ін'єкцію через sortDirection (CVE-2026-65971, виправлено в 6.10.4).
А у власному коді - той самий пошук, тільки по orWhere: кожен, що стоїть не всередині where(function ...), вартий другого погляду.