Skip to Content

What Happens When You Move ERP Business Logic to React and Firebase

A forensic post-mortem of how serverless thick-client architectures compromise ERP databases, expose corporate credentials, and collapse transactional integrity.

Over the past three years, an aggressive architectural anti-pattern has infected enterprise software development across European mid-market organizations. Pressed by high SaaS subscription fees and rigid proprietary workflows, leadership teams increasingly commission boutique software agencies and rapid development studios to build "bespoke, lightweight ERP and CRM systems" within six to eight weeks.

On the surface, the deliverable appears stunning. Built on modern frontend toolchains like React, Next.js, and Vite, the user interface is snappy, visually polished, and responsive. It features dark-mode styling, drag-and-drop dispatch boards, real-time messaging, and interactive analytics. Executives frequently marvel at how quickly their custom operations platform went from wireframe to deployment without the perceived overhead of legacy enterprise platforms.

However, when our forensics team at dlab.md conducts pre-migration code reviews and infrastructure security audits on these environments, we uncover an alarming reality: these applications possess no server-side application tier.

Instead of routing operational commands through a hardened business logic layer, the development agency shifted the entire burden of data validation, authorization, and financial math directly into client-side JavaScript bundles running within the user's browser. The database (typically Google Cloud Firestore or Supabase) is exposed directly to the public internet, shielded only by trivial client-side UI filters and permissive declarative rules.

In a recent forensic engagement involving a commercial platform managing 42,825 customer records, 2,909 active purchase orders, and a workforce of 55 dispatch technicians, an unauthenticated external observer was separated from total database exfiltration and full administrative compromise by exactly two standard HTTP requests.

This technical whitepaper provides an exhaustive breakdown of the failure modes inherent in serverless thick-client enterprise architectures, analyzes the mechanics of Client-Side Mass Assignment (CWE-915) and Broken Object-Level Authorization (OWASP API1:2023), and demonstrates how Odoo 19 CE and PostgreSQL enforce immutable Zero Client-Side Trust at the kernel layer.

Forensic Audit Findings: Serverless Thick-Client Architecture

Telemetry extracted during authorized security audit across 42,825 client records

42,825
Customer PII Records
Full names, residential addresses, and phone numbers exposed via unauthenticated endpoints
2
HTTP Requests to Root
Total requests required for an unauthenticated attacker to obtain super-admin privileges
0
Server-Side Guards
Zero backend API validation layers between client React bundles and production database
200ms
Privilege Escalation Latency
Time required to elevate a low-privileged technician token to full administrative ownership

The Anatomy of the Thick-Client Anti-Pattern


In enterprise systems engineering, the division between presentation, business logic, and persistence is non-negotiable. A robust architecture enforces Zero Client-Side Trust: the client browser is treated as an inherently hostile, untrusted terminal. The browser is responsible solely for rendering UI components and collecting user inputs. It has zero direct access to the database wire protocol, no knowledge of database schema constraints, and no authority to declare whether a financial balance is correct or whether a record modification is legitimate.

In a serverless thick-client architecture, this entire separation principle is discarded. To accelerate frontend velocity and bypass the engineering effort required to build a structured API gateway with strict transactional models, developers establish direct client-to-database connections.

Architecture Comparison: Thick-Client Anti-Pattern vs. Enterprise Zero-Trust

Serverless Thick-Client (React + Direct DB)

Compromised Anti-Pattern
  • Database schema and collections exposed directly in public JS bundles
  • Authorization enforced via client-side UI toggles (if user.role === 'admin')
  • Direct client-to-database write operations (updateDoc, setDoc, REST)
  • Susceptible to CWE-915 Mass Assignment and CWE-639 BOLA
  • Non-transactional NoSQL document mutations causing financial ledger drift

Odoo 19 CE + PostgreSQL Zero-Trust Core

Enterprise Standard
  • Zero direct database access; PostgreSQL bound to internal 127.0.0.1 socket
  • Mandatory three-tier server enforcement: ir.model.access, ir.rule, and field ACL
  • Server-side atomic ACID transactions with strict row locks (FOR UPDATE)
  • Cryptographic UUID tokens (const_eq) for external portal and document access
  • Automated security trimming eliminating sensitive fields prior to JSON-RPC transmission

When an enterprise replaces an application server with direct client-side database calls, security boundaries dissolve. We documented this breakdown across four sequential attack vectors during our forensic investigation.

Unauthenticated Schema Discovery and BOLA (CWE-540 & CWE-639)


When a visitor navigates to the public login screen of a thick-client web application, the HTTP server responds with the pre-compiled JavaScript bundle. Because modern build pipelines bundle application route trees together unless complex code-splitting and dynamic imports are strictly architected, the client downloads the code for the entire administrative back-office before a single credential is submitted.

Inspecting the main bundle via standard browser developer tools immediately revealed the full enterprise administrative surface:

  • The PayrollPage.js module contained complete compensation formulas, commission calculations, and disciplinary penalty structures.
  • The FinancesPage.js and finance.service.js files exposed accounting ledger schemas, payment gateway credentials, cash desk balances, and vendor reconciliation routines.
  • The SettingsPage.js bundle held infrastructure configurations, third-party webhook URLs, and telephony credentials.

Because query builders and collection names were declared directly within frontend service files, an external observer mapped all 36 database collections within sixty seconds.

Simultaneously, the frontend exposed a public order lookup function designed for customer self-service: https://app.client-domain.com/getOrder?orderNumber=ORD-202508-0117.

This endpoint suffered from classic Broken Object-Level Authorization (OWASP API1:2023 / CWE-639). The serverless function failed to authenticate incoming requests or verify whether the requester was authorized to view the specific order. Because purchase order numbers were generated as simple sequential integers padded with a date stamp, an automated shell script could enumerate every transaction in the enterprise database. In less than forty minutes, the entire history of 42,825 customer profiles (including physical residential addresses, telephone numbers, order totals, and dispatch notes) was vulnerable to systematic exfiltration.

Public Identity Provisioning and Identity Theft (CWE-250)


The standard defense mounted by proponents of serverless databases is declarative security rules: allow read, write: if request.auth != null;. Developers assume that as long as unauthenticated visitors cannot read the database directly, the data remains secure.

This assumption collapsed due to a developer convenience shortcut.

To allow administrators to onboard field dispatch personnel and contractors quickly from within the web interface, the original engineering agency implemented an inline registration modal. However, rather than writing a privileged server-side Cloud Function utilizing the administrative SDK (firebase-admin), the frontend developer directly invoked the client SDK method:

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

export async function registerFieldTechnician(email, temporaryPassword) {
const auth = getAuth();
// Insecure client-side invocation requiring public registration to remain open
const userCredential = await createUserWithEmailAndPassword(auth, email, temporaryPassword);
return userCredential.user;
}

For this browser-side function to operate, the enterprise project's identity provider was required to keep public self-registration (accounts:signUp) permanently enabled.

Consequently, any external adversary could extract the public project configuration from the page source, construct an HTTP POST request to the identity provider endpoint with an arbitrary email address, and receive a cryptographically valid authentication token within 200 milliseconds.

The boundary condition request.auth != null was completely neutralized. An anonymous external entity instantly transformed into a recognized authenticated user on the corporate tenant.

The Secret Repository in settings/* (CWE-522)


Once an attacker possesses a low-privileged authenticated token, what can they access?

Frontend developers frequently equate UI visibility with database security. In the investigated system, the React navigation bar conditionally rendered the "Settings" tab based on user claims: {currentUser.role === 'admin' && <AdminSettingsMenu />}.

However, the underlying database rules failed to mirror this restriction. The settings collection was configured with a global rule allowing any authenticated user to read all documents: allow read: if request.auth != null;.

Directly querying the settings collection via the browser console yielded catastrophic operational intelligence:

  1. A Google Cloud Service Account Private Key (-----BEGIN PRIVATE KEY-----) possessing full Editor permissions across the enterprise Google Cloud environment granted complete read/write authority over marketing pipelines, cloud storage buckets, and BigQuery analytics data.
  2. Production SMS Gateway API Credentials allowed unauthorized parties to dispatch spoofed SMS messages under the corporate alphanumeric sender ID at company expense.
  3. High-Resolution Executive Signatures and Company Seals were stored as publicly accessible, unexpiring Cloud Storage URLs pointing to transparency masks of the company seal and Chief Financial Officer's signature.
  4. Cleartext Technician Credentials were exposed because the frontend service had systematically saved generated technician passwords directly into customer dispatch cards under the property installerPassword, exposing all 55 field operators to credential harvesting.

Client-Side Mass Assignment to SuperAdmin (CWE-915)


The ultimate collapse of this architecture occurred at the account profile endpoint, demonstrating a textbook implementation of Client-Side Mass Assignment (CWE-915).

In the user profile management service (user.service.ts), account update requests were written using an unconstrained JavaScript object spread operator:

// Vulnerable client-side mutation pattern
async function updateUserProfile(userId, clientPayload) {
const userDocRef = doc(databaseInstance, "users", userId);
// Unfiltered payload spread directly into database document
await updateDoc(userDocRef, {
...clientPayload,
updatedAt: serverTimestamp()
});
}

Because access control was evaluated exclusively in the React rendering tree and the database security rules omitted field-level write constraints (allow write: if request.auth.uid == userId;), the database accepted any schema property submitted in the mutation payload.

An attacker simply executed the following script directly within the browser console:

// Exploit payload executed in client console
const auth = getAuth();
const currentUid = auth.currentUser.uid;

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

Within 200 milliseconds, the database document was updated. Upon reloading the page, the React routing engine evaluated the newly assigned role: "super_admin", rendered the full executive dashboard, and granted unrestricted read and write authority across every operational, financial, and personnel record in the company.

Vulnerability vs. Remediation: Client Mutation vs. Odoo 19 ORM Gate

// Insecure: Direct client-side mutation with unconstrained spread operator
import { doc, updateDoc, serverTimestamp } from "firebase/firestore";

export async function updateEmployeeRecord(targetId, userSubmittedData) {
// Any property in userSubmittedData (e.g. role: 'admin', salary: 99999)
// is written directly to the production document.
const recordRef = doc(db, "employees", targetId);
await updateDoc(recordRef, {
...userSubmittedData,
updatedAt: serverTimestamp()
});
}
# Secure: Server-side validation with field-level ACL and strict write protection
from odoo import models, fields, api, exceptions, _

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

# Security role is strictly restricted to System Administrators
security_role = fields.Selection([
('technician', 'Field Technician'),
('dispatcher', 'Operations Dispatcher'),
('manager', 'Branch Manager'),
('admin', 'System Administrator')
], string="Authorization Tier", 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(_("Security roles can only be provisioned by System Administrators."))
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(_("Modifying authorization tiers requires super-user privileges."))
return super().write(vals)

The Zero Client-Side Trust Architecture in Odoo 19 & PostgreSQL


To eliminate CWE-915, CWE-540, and BOLA vulnerabilities permanently, the software core must enforce security at the database and application server kernel. In Odoo 19 CE backed by PostgreSQL, security is organized as an immutable multi-layered gateway.

Odoo 19 Multi-Layered Server Security Gateway

01

Network & Port Isolation

PostgreSQL binds strictly to 127.0.0.1 or private VPC subnets. Public client browsers cannot establish socket connections to the database under any circumstance.

02

Model Access Layer (ir.model.access)

Evaluates table-level Read, Write, Create, and Unlink privileges based on user security groups before constructing query ASTs.

03

Row-Level Security (ir.rule)

Server intercepts the ORM domain and injects mandatory SQL WHERE clauses, ensuring technicians only retrieve records assigned to their user ID or company boundary.

04

Field-Level Security & Trimming

Fields decorated with groups='base.group_system' or restricted attributes are purged from the server response payload prior to network serialization.

Model-Level Access Control (ir.model.access.csv)

In Odoo, no model can be read or written via JSON-RPC unless explicit permissions are assigned in ir.model.access.csv. If an unauthenticated user or an unauthorized role invokes execute_kw on an administrative model (such as account.move or hr.payroll), the Python kernel rejects the call with an immediate AccessError before database transactions begin.

Mandatory SQL Injection of Row-Level Security (ir.rule)

When a field technician requests tasks via the Odoo mobile interface, the client does not submit a query containing an easily manipulable filter like where technician_id = 42.

Instead, the Odoo ORM dynamically intercepts the request and appends server-enforced SQL clauses based on active ir.rule definitions:

<record id="fsm_technician_task_rule" model="ir.rule">
<field name="name">Technicians view only assigned dispatch tasks</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>

Even if a rogue actor manipulates JSON-RPC arguments to query domain: [] (retrieve all rows), the ORM enforces the domain_force criteria at the SQL level: WHERE project_task.id IN (SELECT task_id FROM project_task_user_rel WHERE user_id = current_uid). Database-level isolation remains absolute.

Native Protection Against Mass Assignment

Odoo models strictly define writeable properties. In Odoo 19, the ORM incorporates SELF_WRITEABLE_FIELDS on user models. An authenticated user updating their own profile can modify cosmetic attributes (such as lang or tz), but submitting security-sensitive fields (such as groups_id, company_id, or partner_id) raises a server-side exception regardless of the client-side payload structure.

Below is an executable PostgreSQL SQL assertion and trigger pattern that provides defense-in-depth at the raw database layer, preventing unauthorized role elevation even if an application-layer bug were introduced:

-- PostgreSQL Enterprise Defense-in-Depth: Security Role Mutation Guard
CREATE OR REPLACE FUNCTION verify_user_privilege_escalation()
RETURNS TRIGGER AS $$
BEGIN
-- Prevent modification of security groups unless executed by super-administrator
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 'SECURITY ALERT: Unauthorized attempt to elevate role for user % from % to %',
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();

When NOT to Use Serverless Thick-Clients and Architectural Trade-Offs


To maintain rigorous engineering objectivity, system architects must evaluate when serverless architectures are appropriate and when they represent catastrophic risk.

Workload Suitability Matrix: Serverless Thick-Client vs. Relational ERP

Acceptable for Serverless Thick-Clients

Low Risk
  • Public marketing microsites with read-only product catalogs
  • Ephemeral proof-of-concept prototypes discarded after user testing
  • Collaborative whiteboard tools with non-financial document models
  • Personal single-user productivity utilities with no multi-tenancy
  • Static documentation portals and informational knowledge bases

Mandatory for Relational ERP (Odoo 19 / PostgreSQL)

Enterprise Grade
  • Financial accounting requiring double-entry ledger balancing (ACID)
  • Multi-warehouse inventory with serial number tracking and negative stock guards
  • Field service dispatch involving customer PII and technician payroll math
  • B2B client portals processing credit cards, invoices, and contracts
  • Enterprise compliance environments subject to GDPR, ISO 27001, or SOC 2

Architectural Trade-Off Analysis

  1. Balancing velocity against long-term TCO shows that while building an operational prototype in React and Firebase can be accomplished in weeks because developers omit data access layers, relational migrations, and server-side state machines, the technical debt accumulates exponentially. Once a business requires double-entry accounting, audit trails, and multi-entity consolidation, retrofitting security rules onto hundreds of client-side Firestore queries becomes mathematically untenable.
  2. Comparing declarative rule complexity against a procedural ORM highlights how Firestore security rules are written in a domain-specific language that lacks support for complex relational queries, recursive group hierarchies, and procedural rollback hooks. Attempting to enforce multi-company isolation rules (check_company) in Firestore rules results in thousands of lines of fragile, unmaintainable JSON declarations that exceed cloud vendor rule evaluation depth limits.
  3. Evaluating execution cost predictability reveals that serverless document stores charge per document read, write, and bandwidth unit. In an operational ERP, recalculating dispatch route statuses or inventory reconciliations across thousands of rows can trigger millions of un-cached document reads, transforming a low-cost database into a five-figure monthly cloud billing surprise.

The 5-Minute Executive Security Checklist for CTOs and CISOs


If your organization is currently operating or evaluating a bespoke web platform built on modern frontend frameworks, execute this five-minute audit protocol before placing financial records or customer data into production:

  1. Inspect network telemetry for direct database calls by opening browser developer tools (F12), navigating to the Network tab, and filtering requests by WS (WebSocket) or XHR. If you observe continuous outbound connections to firestore.googleapis.com, supabase.co, or direct REST mutations modifying collections (/documents/users/...), your system is operating as an insecure thick-client.
  2. Review public bundle manifests by searching the downloaded .js source files for administrative terms (payroll, invoice, settings, private_key). If internal administrative route structures and business logic functions are present in the bundles delivered to anonymous visitors, your system violates the principle of least privilege (CWE-540).
  3. Audit the self-registration perimeter by attempting to register an account using an arbitrary email address against your authentication API endpoint without an administrative invitation token. If the identity provider returns a valid session token, verify whether your database rules permit the newly created identity to read internal operational documents.
  4. Test for parameter tampering (CWE-915) by intercepting a routine user profile update request (e.g., updating a phone number) and injecting administrative properties: {"role": "admin", "is_admin": true, "organization_id": 1}. If the database persists these values without server-side validation errors, your entire authorization model is compromised.

Strategic Recommendations for Hardened ERP Infrastructure


Understanding the root cause of CWE-915 (Mass Assignment) and Broken Object-Level Authorization (BOLA) clarifies why declarative NoSQL rules cannot protect a commercial ERP. CWE-915 arises when applications accept user-submitted request payloads and bind them directly into database objects without explicit attribute whitelisting. While Firestore rules can enforce basic schemas, they lack native support for relational ACID transactions, multi-table joins, complex recursive access trees, and automated server-side security trimming. In contrast, Odoo 19 eliminates BOLA through its portal.mixin and Row-Level Security (ir.rule) engines, requiring a cryptographically random 36-character UUID access_token evaluated via constant-time comparison (const_eq) for unauthenticated document access and automatically filtering authenticated queries at the SQL construction layer.

For organizations currently trapped in a thick-client architecture, the immediate emergency remediation is to lock down database security rules against direct browser writes, disable public self-registration in the identity provider, and route all mutations through authenticated server-side functions. The permanent strategic solution is migrating the operational core to an enterprise-grade ERP such as Odoo 19 CE on dedicated virtual infrastructure backed by PostgreSQL, where business logic, relational integrity, and Zero Client-Side Trust are enforced natively at the kernel layer.

To evaluate your current system perimeter or plan an architectural migration to hardened Odoo 19 enterprise infrastructure, contact the systems forensics team at dlab.md.

Architecture & Security Due Diligence dlab.md Engineering Advisory

Preparing Your AI & ERP Stack for Enterprise Security Audits?

Our systems architects conduct fixed-scope 10-Day Zero-Trust Architecture Audits (OAuth 2.0 CAB, EU AI Act Art. 12/14, GDPR Art. 25, PostgreSQL & Odoo 19 hardening) with a complete remediation roadmap.