Перейти к содержимому

Чем заканчивается вынос бизнес-логики ERP во фронтенд на примере реального аудита

Криминалистический пост-мортем архитектурных дефектов: как бессерверные thick-client решения компрометируют базы данных ERP, раскрывают корпоративные секреты и разрушают транзакционную модель.

За последние три года на рынках корпоративного программного обеспечения Восточной и Центральной Европы (включая Молдову, Румынию и Польшу) сформировался устойчивый и опасный тренд. Столкнувшись с высокими тарифами на ежемесячные подписки зарубежных облачных SaaS-систем и негибкостью типовых конфигураций, собственники растущих торговых и сервисных предприятий всё чаще заказывают разработку «собственной уникальной CRM и ERP под ключ» у небольших веб-студий и команд быстрой разработки с обещанием готовности за 6–8 недель.

На демонстрационном показе для руководства такой программный комплекс выглядит безупречно. Построенный на современных инструментах фронтенд-разработки (React, Next.js, Vite), он демонстрирует мгновенный отклик интерфейса, плавную анимацию, канбан-доски распределения нарядов, тёмную тему оформления и прямую интеграцию с мессенджерами. Руководство искренне радуется тому, как быстро прототип перешёл в рабочую стадию без длительного проектирования и высоких затрат, традиционно сопровождающих промышленные ERP-системы.

Однако когда экспертная группа dlab.md подключается для проведения предмиграционного аудита исходного кода и серверной инфраструктуры перед масштабированием бизнеса, мы регулярно сталкиваемся с одной и той же фундаментальной проблемой: у подобной системы отсутствует серверный бэкенд как таковой.

Вместо того чтобы пропускать финансовые операции и действия пользователей через защищённый серверный уровень бизнес-логики, разработчики переложили всю задачу валидации данных, расчёта стоимости и контроля разграничения прав непосредственно на JavaScript-бандлы, исполняемые в браузере посетителя. База данных (как правило, Google Cloud Firestore или аналогичное NoSQL-хранилище) выставлена напрямую в публичный интернет и защищена лишь условными переключателями в интерфейсе и тривиальными декларативными правилами.

В ходе недавнего практического аудита коммерческой платформы, обслуживающей базу из 42 825 клиентов, 2 909 активных заказов и штат из 55 выездных инженеров, любого внешнего неавторизованного наблюдателя отделяли от полного скачивания базы данных и захвата прав супер-администратора ровно два стандартных HTTP-запроса.

В этом техническом отчёте подробно разбираются причины деградации бессерверных thick-client архитектур, анатомия уязвимостей массового присвоения атрибутов (CWE-915) и поломки объектной авторизации (OWASP API1:2023 / CWE-639), а также стандарты серверной изоляции Zero Client-Side Trust на базе Odoo 19 CE и PostgreSQL.

Криминалистические метрики аудита Serverless Thick-Client

Телеметрия авторизованного аудита безопасности на выборке из 42 825 записей

42 825
Персональных данных (PII)
ФИО, телефоны и домашние адреса клиентов, доступные без авторизации
2
HTTP-запроса до Root
Количество запросов, требовавшихся человеку с улицы для получения прав SuperAdmin
0
Серверных барьеров
Полное отсутствие промежуточного слоя валидации между браузером и боевой БД
200 мс
Время эскалации прав
Скорость наделения низкопривилегированного аккаунта неограниченными полномочиями

Анатомия анти-паттерна Serverless Thick-Client


В классической теории построения информационных систем разделение уровней представления, прикладной бизнес-логики и хранения данных является абсолютным правилом. Надежная архитектура проектируется на принципе Zero Client-Side Trust (Нулевое доверие к клиенту): веб-браузер или мобильное приложение рассматриваются как потенциально скомпрометированный и ненадёжный терминал. Задача клиента состоит исключительно в отрисовке элементов пользовательского интерфейса и передаче команд пользователя. Браузер не должен иметь прямого сетевого доступа к протоколу базы данных, не должен определять корректность финансового баланса и не имеет права самостоятельно решать, имеет ли пользователь доступ к той или иной записи.

В архитектуре бессерверного «толстого клиента» (Serverless Thick-Client SPA) этот канон грубо нарушается. Желая ускорить сдачу проекта и сэкономить ресурсы на разработке API-шлюза с транзакционными проверками, студии подключают клиентские компоненты React напрямую к облачной базе данных через публичные клиентские библиотеки.

Сравнение архитектур: Thick-Client против промышленного ядра Odoo 19

Serverless Thick-Client (React + прямой NoSQL)

Опасный анти-паттерн
  • Структура таблиц и схема коллекций раскрыты в открытом клиентском JS
  • Проверка прав реализована через интерфейсные условия (if user.role === 'admin')
  • Прямые мутации базы данных из браузера (updateDoc, setDoc, NoSQL REST)
  • Уязвимость к массовому присвоению (CWE-915) и перебору объектов BOLA (CWE-639)
  • Документные NoSQL-записи без поддержки ACID, приводящие к кассовым разрывам

Odoo 19 CE + PostgreSQL (Zero-Trust Core)

Промышленный стандарт
  • Полный запрет прямого доступа к СУБД; PostgreSQL замкнут на сокет 127.0.0.1
  • Трёхуровневая серверная валидация: ir.model.access, ir.rule и группы на полях
  • Атомарные транзакции ACID с явными блокировками строк (SELECT FOR UPDATE)
  • Криптографические токены UUIDv4 с проверкой за константное время (const_eq)
  • Автоматическая вырезка конфиденциальных полей до сериализации сетевого ответа

Когда предприятие устраняет серверный шлюз приложений в пользу прямого общения браузера с базой данных, рубежи обороны исчезают. В ходе нашего расследования мы зафиксировали последовательное прохождение четырёх фаз полной компрометации системы.

Раскрытие схемы в публичном бандле и перебор заказов BOLA (CWE-540 и CWE-639)


Когда пользователь открывает страницу входа в систему (/login), веб-сервер отдает ему готовый скомпилированный JavaScript-файл. Поскольку в стандартных сборщиках фронтенда (таких как Vite или Webpack) оптимизация разделения кода по ролям требует кропотливой ручной настройки, браузер любого неавторизованного посетителя скачивает код всей административной панели ещё до ввода пароля.

Анализ главного скрипта через стандартную консоль разработчика браузера (F12) сразу выявил структуру закрытых корпоративных разделов:

  • Модуль PayrollPage.js содержал формулы расчёта заработной платы, штрафных санкций, бонусов и сдельных ставок;
  • Файлы FinancesPage.js и finance.service.js открывали план внутренних счетов, кассы предприятия, реквизиты платёжных шлюзов и алгоритмы сверки взаиморасчётов;
  • Скрипт SettingsPage.js включал системные настройки инфраструктуры, ключи интеграций и авторизационные параметры корпоративной телефонии.

Поскольку обращения к коллекциям базы данных формировались непосредственно в сервисных функциях фронтенда, сторонний наблюдатель за 60 секунд восстановил карту всех 36 коллекций базы данных.

Параллельно система содержала публичный эндпоинт проверки статуса наряда для клиентов: https://app.client-domain.com/getOrder?orderNumber=ORD-202508-0117.

Этот эндпоинт страдал от критической уязвимости Broken Object-Level Authorization (OWASP API1:2023 / CWE-639). Облачная функция не проверяла личность инициатора запроса и не сопоставляла его с владельцем заказа. Поскольку номера заказов генерировались как инкрементальные порядковые числа с датой, простейший циклический скрипт позволил последовательно скачать весь архив транзакций. За сорок минут база из 42 825 клиентов компании (с именами, домашними адресами, телефонами и финансовыми суммами) оказалась полностью выгружена.

Публичная саморегистрация и обход базовых фильтров (CWE-250)


Сторонники бессерверных баз данных нередко апеллируют к декларативным правилам безопасности: allow read, write: if request.auth != null;. Предполагается, что пока внешний пользователь не авторизован, база данных надёжно защищена от несанкционированного чтения.

Однако эта иллюзия безопасности разрушилась из-за технического компромисса, допущенного разработчиками ради удобства.

Чтобы операторы могли быстро регистрировать новых сотрудников прямо из интерфейса, разработчики добавили форму создания пользователя. Но вместо того чтобы реализовать эту логику на сервере через защищённую функцию с использованием административного SDK (firebase-admin), они вызвали клиентский метод SDK непосредственно в коде страницы:

import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";

export async function registerFieldTechnician(email, temporaryPassword) {
const auth = getAuth();
// Опасный вызов на клиенте, потребовавший держать открытой публичную регистрацию
const userCredential = await createUserWithEmailAndPassword(auth, email, temporaryPassword);
return userCredential.user;
}

Чтобы этот код мог выполняться в браузере рядового пользователя, в настройках службы аутентификации Google Cloud Identity Toolkit разработчикам пришлось оставить постоянно включённой публичную саморегистрацию (accounts:signUp).

В результате любой злоумышленник извлекает открытый ключ проекта (apiKey) из HTML-кода страницы, отправляет типовой POST-запрос на регистрацию с произвольным адресом электронной почты и через 200 миллисекунд получает легитимный токен авторизации. Правило request.auth != null оказывается удовлетворено. Барьер между посторонним человеком и внутренним сотрудником перестаёт существовать.

Кладбище конфиденциальных данных в коллекции settings/* (CWE-522)


Что становится доступно обладателю базового авторизационного токена?

Создатели интерфейсов часто путают визуальное сокрытие элементов с реальной безопасностью данных. В исследуемой системе видимость вкладки «Настройки» в меню определялась проверкой роли: {currentUser.role === 'admin' && <AdminSettingsMenu />}.

Однако в правилах безопасности базы данных для коллекции settings действовало общее разрешающее правило: allow read: if request.auth != null;.

Прямой запрос к документам этой коллекции из консоли браузера позволил извлечь критические активы компании:

  1. Приватный криптографический ключ сервисного аккаунта Google Cloud (-----BEGIN PRIVATE KEY-----) обладал правами редактора (Editor) на весь облачный проект, предоставляя полный доступ на чтение и запись к корпоративным хранилищам Cloud Storage и аналитической базе BigQuery;
  2. Боевой API-ключ шлюза SMS-рассылок позволял отправлять любые сообщения от официального альфанумерического имени компании за счёт её бюджета;
  3. Прямые ссылки на оригиналы печати организации и подписи финансового директора вели на файлы в высоком разрешении с бессрочными токенами доступа в открытом бакете;
  4. Пароли 55 линейных сотрудников в открытом виде (installerPassword) оказались доступны, поскольку фронтенд автоматически сохранял сгенерированные учётные данные прямо в теле карточек клиентов для удобства повторного отображения.

Клиентское массовое присвоение и мгновенный захват прав SuperAdmin (CWE-915)


Завершающим этапом взлома стала эксплуатация уязвимости Client-Side Mass Assignment (CWE-915) в модуле управления профилем.

В сервисе обновления учётных данных (user.service.ts) функция отправки изменений была написана с использованием оператора расширения (...payload) без какой-либо фильтрации разрешённых полей:

// Уязвимый шаблон обновления данных на стороне клиента
async function updateUserProfile(userId, clientPayload) {
const userDocRef = doc(databaseInstance, "users", userId);
// Нефильтрованный объект записывается прямо в документ базы данных
await updateDoc(userDocRef, {
...clientPayload,
updatedAt: serverTimestamp()
});
}

Поскольку проверка статуса администратора существовала только в React-рендерере, а в правилах базы данных пользователю разрешалось изменять свой собственный документ (allow write: if request.auth.uid == userId;), база данных без возражений сохраняла любые переданные параметры.

Аудитор выполнил в консоли браузера следующую команду:

// Эксплуатация уязвимости CWE-915 напрямую из консоли браузера
const auth = getAuth();
const currentUid = auth.currentUser.uid;

await updateDoc(doc(db, "users", currentUid), {
role: "super_admin",
isVerified: true,
accessLevel: 100,
permissions: ["ALL_PRIVILEGES"]
});

Спустя 200 миллисекунд документ в базе данных обновился. При перезагрузке страницы фронтенд прочитал присвоенную роль super_admin, отобразил полный административный функционал и предоставил неограниченные полномочия по управлению всеми финансовыми и операционными процессами предприятия.

Сравнение реализации: Клиентский код против серверного ядра Odoo 19

// Небезопасно: Прямая запись из браузера с неконтролируемым spread-оператором
import { doc, updateDoc, serverTimestamp } from "firebase/firestore";

export async function updateEmployeeRecord(targetId, userSubmittedData) {
// Любое поле из переданного объекта (например, role: 'admin')
// попадает напрямую в боевой документ базы данных.
const recordRef = doc(db, "employees", targetId);
await updateDoc(recordRef, {
...userSubmittedData,
updatedAt: serverTimestamp()
});
}
# Безопасно: Серверный контроль прав, проверка групп и блокировка записи
from odoo import models, fields, api, exceptions, _

class ResUsersCustom(models.Model):
_inherit = 'res.users'

# Роль безопасности защищена на уровне метаданных: доступна только Администраторам
security_role = fields.Selection([
('technician', 'Выездной инженер'),
('dispatcher', 'Диспетчер смены'),
('manager', 'Руководитель подразделения'),
('admin', 'Системный администратор')
], string="Уровень доступа", default='technician',
groups="base.group_system", tracking=True)

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if 'security_role' in vals and not self.env.user.has_group('base.group_system'):
raise exceptions.AccessError(_("Назначение уровней доступа разрешено только Администраторам."))
return super().create(vals_list)

def write(self, vals):
if 'security_role' in vals and not self.env.user.has_group('base.group_system'):
raise exceptions.AccessError(_("Изменение системной роли требует прав супер-пользователя."))
return super().write(vals)

Архитектура Zero Client-Side Trust в Odoo 19 и PostgreSQL


Для гарантированного устранения уязвимостей классов CWE-915, CWE-540 и BOLA система должна опираться на серверные барьеры, внедрённые в ядро СУБД и сервер приложений. В Odoo 19 CE под управлением PostgreSQL безопасность организована как многоуровневый фильтрационный контур.

Эшелонированная защита серверного ядра Odoo 19

01

Сетевая изоляция СУБД

PostgreSQL слушает исключительно локальный интерфейс 127.0.0.1. Прямое сетевое соединение из браузера с базой данных технически невозможно.

02

Контроль доступа к моделям (ir.model.access)

Проверяет права группы пользователя на таблицу (чтение, запись, создание, удаление) до генерации SQL-запроса.

03

Безопасность на уровне строк (ir.rule)

Сервер модифицирует абстрактное синтаксическое дерево запроса и принудительно внедряет условия WHERE для изоляции чужих данных.

04

Фильтрация на уровне полей (Field ACL)

Секретные атрибуты, системные токены и себестоимость физически вырезаются из сетевого ответа перед отправкой клиенту.

Табличный контроль доступа (ir.model.access.csv)

В Odoo ни одна таблица не может быть прочитана или модифицирована через сетевой интерфейс JSON-RPC без явного разрешения в матрице доступа. Если неавторизованный пользователь попытается вызвать методы моделей account.move или hr.payslip, ядро прерывает выполнение с ошибкой AccessError до открытия транзакции в базе данных.

Принудительное внедрение условий SQL (ir.rule)

Когда выездной специалист запрашивает перечень нарядов в мобильном интерфейсе Odoo, клиент не передает ненадежный фильтр вроде where technician_id = 42.

Вместо этого серверный ORM перехватывает обращение и принудительно дополняет формируемый SQL-запрос внутренними правилами безопасности:

<record id="fsm_technician_task_rule" model="ir.rule">
<field name="name">Сотрудники видят только назначенные наряды</field>
<field name="model_id" ref="model_project_task"/>
<field name="domain_force">[('user_ids', 'in', [user.id])]</field>
<field name="groups" eval="[(4, ref('project.group_project_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>

Даже если атакующий передаст пустой домен поиска (domain: []) с целью выгрузить все наряды компании, ядро Odoo жестко ограничит выборку: WHERE project_task.id IN (SELECT task_id FROM project_task_user_rel WHERE user_id = current_uid). Прорыв изолированного контекста исключен на уровне математики реляционной алгебры.

Встроенная защита от массового присвоения

Модели Odoo жестко регламентируют разрешенные к записи поля. В Odoo 19 атрибут SELF_WRITEABLE_FIELDS на модели пользователей строго ограничивает перечень свойств, которые сотрудник может обновить в своем профиле (язык, часовой пояс). Любая попытка передать поля доступа (groups_id) или принадлежности к филиалам (company_id) вызывает немедленный отказ в обслуживании.

Для обеспечения глубоко эшелонированной обороны (Defense-in-Depth) на уровне самого сервера PostgreSQL мы применяем триггеры, пресекающие несанкционированную смену ролей:

-- Эшелонированная защита на уровне ядра PostgreSQL
CREATE OR REPLACE FUNCTION verify_user_privilege_escalation()
RETURNS TRIGGER AS $$
BEGIN
-- Запрет изменения привилегированной роли кем-либо, кроме супер-администратора
IF NEW.security_role IS DISTINCT FROM OLD.security_role THEN
IF CURRENT_USER != 'odoo_master_admin' AND
NOT EXISTS (
SELECT 1 FROM res_groups_users_rel r
JOIN res_groups g ON r.gid = g.id
WHERE r.uid = (SELECT id FROM res_users WHERE login = CURRENT_USER)
AND g.category_id = (SELECT id FROM ir_module_category WHERE name = 'Administration')
) THEN
RAISE EXCEPTION 'ОТКАЗ В ДОСТУПЕ: Попытка неавторизованного изменения роли пользователя % с % на %',
OLD.id, OLD.security_role, NEW.security_role
USING ERRCODE = '28000';
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE OR REPLACE TRIGGER trg_protect_security_roles
BEFORE UPDATE ON res_users
FOR EACH ROW
EXECUTE FUNCTION verify_user_privilege_escalation();

Когда Serverless допустим, а когда строго запрещён


Для сохранения инженерной объективности системный архитектор обязан трезво сопоставлять границы применимости технологий.

Матрица применимости: Serverless Thick-Client против реляционной ERP

Допустимо для Serverless Thick-Client

Низкий риск
  • Промо-лендинги и витрины с каталогами только для чтения
  • Одноразовые маркетинговые прототипы для проверки продуктовых гипотез
  • Интерактивные канвас-доски без финансовой модели и личных данных
  • Персональные трекеры привычек без корпоративной мультиарендности
  • Базы знаний и документационные порталы со статическим контентом

Обязательно для реляционной ERP (Odoo 19 / PostgreSQL)

Enterprise класс
  • Бухгалтерский и управленческий учёт с принципом двойной записи (ACID)
  • Складской учёт с контролем партий, серийных номеров и резервов
  • Диспетчеризация полевых инженеров с расчётом сдельной оплаты труда
  • Клиентские B2B-порталы с биллингом, актами и договорами
  • Соответствие регуляторным требованиям по защите персональных данных (GDPR)

Анализ инженерных компромиссов

  1. Сопоставляя скорость разработки и совокупную стоимость владения (TCO), мы видим, что создание интерфейса на React с прямым NoSQL действительно позволяет запустить первый экран быстрее за счёт отказа от проектирования реляционной схемы. Но плата за это наступает при первой же попытке свести дебет с кредитом. Добавление строгой финансовой логики в сотни разрозненных клиентских компонентов превращается в непреодолимый технический тупик.
  2. Сравнивая декларативные правила и процедурную логику, важно учитывать, что декларативные правила облачных баз данных не приспособлены для сложных межбазовых транзакций, проверок кредитных лимитов и каскадного пересчёта остатков. Попытка закодировать бизнес-правила в тысячах строк JSON-конфигураций приводит к неконтролируемым ошибкам и превышению лимитов облачного провайдера.
  3. Оценивая непредсказуемость затрат на инфраструктуру, стоит помнить, что облачные NoSQL-хранилища тарифицируют каждое чтение и запись документа. В операционной системе групповой пересчёт статусов нарядов или складских остатков порождает миллионы холостых операций чтения, превращая символический счёт за хостинг в неконтролируемую статью расходов.

5-минутный чеклист проверки безопасности для CTO и CISO


Если в вашей компании функционирует заказная CRM или веб-портал, выполните четыре базовых шага проверки перед запуском в промышленную эксплуатацию:

  1. Проверьте сетевой трафик на прямые запросы к базам данных, открыв вкладку Network в консоли браузера (F12) и отфильтровав трафик по типам WS и XHR. Наличие постоянных соединений с доменами firestore.googleapis.com, supabase.co или прямых запросов на изменение документов свидетельствует об уязвимой архитектуре «толстого клиента».
  2. Изучите манифест скачиваемых скриптов, выполнив поиск по загруженным .js-файлам по ключевым словам payroll, invoice, settings или private_key. Присутствие административных маршрутов и служебных структур в коде, отдаваемом анонимному посетителю, нарушает принцип наименьших привилегий (CWE-540).
  3. Проверьте периметр регистрации, попробовав отправить запрос на регистрацию нового аккаунта через форму входа с произвольным email-адресом. Если система успешно создает токен сессии без предварительного приглашения администратора, проверьте, какие коллекции базы данных доступны вновь созданному пользователю.
  4. Проведите тест на массовое присвоение (CWE-915), перехватив штатный запрос обновления личных данных профиля и добавив в тело запроса параметры администратора: {"role": "admin", "is_admin": true, "access_level": 99}. Если сервер сохраняет эти атрибуты без генерации исключения, система полностью открыта для эскалации привилегий.

Стратегические выводы по защите корпоративной архитектуры


Первопричина уязвимости CWE-915 (Mass Assignment) и перебора объектов BOLA кроется в том, что приложение принимает переданный клиентом объект данных и напрямую сохраняет его свойства в базу без валидации по строгому белому списку разрешённых полей. Правила безопасности NoSQL-хранилищ способны проверить базовую авторизацию и тип данных, но лишены поддержки полноценных реляционных транзакций ACID, внешних ключей, блокировок строк и серверного сокрытия закрытых полей. Сложную финансовую логику (например, проверку лимита дебиторской задолженности перед отгрузкой) невозможно выразить в декларативных правилах, тогда как в Odoo 19 доступ к внешним документам через портал защищен механизмом portal.mixin с 36-значным криптографическим UUID-токеном (const_eq), а для авторизованных пользователей правила ir.rule автоматически внедряют SQL-условия WHERE.

Для экстренной ликвидации уязвимостей в существующей thick-client CRM первоочередной мерой является перевод правил базы данных в режим полного запрета записи из браузера, отключение публичной регистрации пользователей и вынос критических операций в изолированные серверные функции. Долгосрочное системное решение заключается в миграции бизнес-процессов на зрелое ERP-ядро Odoo 19 CE под управлением реляционной СУБД PostgreSQL, где принцип Zero Client-Side Trust, сохранность коммерческой тайны и защита активов предприятия обеспечены многоуровневым контуром серверной безопасности.

Для проведения независимой экспертизы защищённости вашей текущей информационной системы или проектирования миграции на промышленное ядро Odoo 19 обратитесь к архитектурной группе dlab.md.

Архитектурный и Security Due Diligence аудит dlab.md Engineering Advisory

Готовите стек ИИ и ERP к корпоративному аудиту безопасности?

Наши системные архитекторы проводят фиксированный 10-дневный Zero-Trust аудит архитектуры (OAuth 2.0 CAB, EU AI Act ст. 12/14, GDPR ст. 25, харденинг PostgreSQL и Odoo 19) с готовой дорожной картой устранения уязвимостей.