Skip to content

fix(order): закрыть служебные колонки черновика от записи с витрины (#860) - #896

Merged
biz87 merged 1 commit into
betafrom
fix/860-web-order-field-guard
Oct 15, 2026
Merged

biz87 merged 1 commit into
betafrom
fix/860-web-order-field-guard

Conversation

@biz87

@biz87 biz87 commented Oct 14, 2026

Copy link
Copy Markdown
Member

Closes #860

Проблема

POST /api/v1/order/add принимает произвольный key и пишет его в черновик, если такая колонка есть у заказа или адреса. Белого списка полей формы нет.

Посетитель с токеном своей корзины подставляет чужой customer_id. При оформлении он не перезаписывается — OrderSubmitHandler подставляет покупателя только если поле пустое. Заказ уезжает в чужой личный кабинет. Тем же путём открыты cost, cart_cost, delivery_cost, status_id, num, token, uuid, properties.

Вторая половина: updateField() писал один ключ сразу в две таблицы, если колонка есть и у заказа, и у адреса. Общие колонки — id, createdon, updatedon, properties.

Решение

Гард в OrderFieldManager::add() и remove(), а не в контроллере. Через менеджер полей проходят все витринные пути записи:

путь как приходит
POST /api/v1/order/add Order::add() → fieldManager->add()
POST /api/v1/order/set Order::set() → тот же add() в цикле
POST /api/v1/order/submit OrderSubmitHandler прогоняет data через add() — в issue не упомянуто, дыра та же
POST /api/v1/order/remove remove('token') обнулял привязку заказа к сессии
сниппет ms3_order $ms3->order->add()

Проверка стоит до msOnBeforeAddToOrder — плагином её не обойти.

Почему список запретный, а не разрешительный

Белый список молча сломал бы магазины с собственными колонками в ms3_orders / ms3_order_addresses, заведёнными через extra fields. На тестовом стенде такая колонка уже есть (address.test_address) и после правки остаётся писаемой. Запретный список закрывает ровно то, чем распоряжаются оформление, пересчёт стоимости и смена статуса.

Ключи вне обеих моделей по-прежнему уходят в properties['_validated'] — кастомные поля формы работают как раньше.

Расширяемость

Константы читаются через static::, поэтому магазин может переопределить списки в подклассе OrderFieldManager и подставить его в DI-ключ ms3_order_field_manager через core/config/ms3.services.d/*.php. Этот каталог живёт вне компонента и переживает обновление; ServiceRegistry::validateClass() требует именно наследника. Системной настройки сознательно нет: ослабить security-границу кликом в админке не должно быть возможно.

updateField()

if + if заменён на if + elseif, заказ главнее. Баг срабатывал и без API: OrderAddressManager при выборе сохранённого адреса сам передаёт в updateField properties заказа и затирал properties адреса.

Проверка на стенде

customer_id status_id token cost properties num user_id  → отклонено
city email order_comment delivery_id payment_id         → записано
my_custom_field                                          → записано в _validated
remove token                                             → 404
set {customer_id, city}                                  → customer_id отклонён, city записан
submit {"data":{"customer_id":1}}                         → заказ #141 ушёл покупателю #30 (probe@example.com),
                                                            у покупателя #1 по-прежнему 27 заказов
updateField($draft,'properties',…)                        → order.properties перезаписан,
                                                            address.properties = {"address_own":"keep-me"}

Подмена класса через ms3.services.d/ проверена отдельно: подкласс, убравший customer_id из списка, открывает это поле, остальные остаются закрытыми. Пробник удалён, защита вернулась.

Тесты

  • OrderFieldManagerProtectedFieldsTest — отказ по каждому закрытому ключу в add() и remove() (событие не вызывается, updateField не вызывается), проходимость полей формы, и сверка с minishop3.mysql.schema.xml: новая колонка в схеме валит тест, пока её осознанно не отнесут к закрытым или к полям формы.
  • OrderDraftManagerUpdateFieldTargetTest — properties только в заказ, адресные колонки в адрес, неизвестный ключ в _validated.

На коде до правки оба падают: 25 ошибок и 1 падение, в адрес уезжает {"address_hash":"abc"}.

Гейты локально: PHPUnit 800/800, smoke 122/122, PHPStan по изменённым файлам чисто.

Изменение поведения

Кастомный фронт, который писал delivery_cost через order/add (самостоятельный расчёт доставки), получит ошибку. Это и есть закрываемая дыра — посетитель мог выставить себе нулевую доставку. Штатная поставка такого не делает: стоимость считают DeliveryService и OrderCostCalculator, а ms3_order.tpl шлёт только имена, контакты, адрес, payment_id, delivery_id, comment, save_address.

Хвост на потом

Приём с подменой ms3_order_field_manager стоит описать в документации (development/services.md уже рассказывает про ms3.services.d/).

…860)

Web API принимал произвольный key и писал его в черновик, если такая
колонка есть у заказа или адреса. Посетитель с токеном своей корзины
подставлял чужой customer_id, при оформлении он уже не перезаписывается,
и заказ уезжал в чужой личный кабинет. Тем же путём открывались cost,
status_id, num, token и properties.

OrderFieldManager::add() и remove() теперь отклоняют служебные колонки.
Гард стоит в менеджере полей, а не в контроллере: через него проходят все
витринные пути — order/add, order/set, order/remove, data в order/submit
и сниппет ms3_order. Проверка до msOnBeforeAddToOrder, плагином её не
обойти.

Список запретный, не разрешительный: кастомные колонки, заведённые через
extra fields, остаются писаемыми, а ключи вне моделей по-прежнему уходят
в properties['_validated']. Константы читаются через static::, поэтому
магазин может переопределить списки в подклассе и подставить его в
ms3_order_field_manager через core/config/ms3.services.d/ — этот каталог
живёт вне компонента и переживает обновление.

Вторая половина issue: updateField() писал один ключ сразу в две таблицы,
если колонка есть и у заказа, и у адреса (id, createdon, updatedon,
properties). Из-за этого properties адреса затирались свойствами заказа —
в том числе при обычном выборе сохранённого адреса, где properties заказа
передаёт сам OrderAddressManager. Теперь заказ главнее и запись идёт в
одну таблицу.

Изменение поведения: кастомный фронт, который писал delivery_cost через
order/add, получит ошибку — именно через это посетитель мог выставить
себе нулевую доставку. Штатная поставка такого не делает, стоимость
считают DeliveryService и OrderCostCalculator.
@biz87
biz87 merged commit 614baa0 into beta Oct 15, 2026
6 checks passed
@biz87
biz87 deleted the fix/860-web-order-field-guard branch October 15, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Web API order/add записывает любую колонку черновика, в том числе customer_id

1 participant