October 24, 2026
How I Fixed an OTP Leak Vulnerability in Mendix β And How You Can Too
A real-world account takeover vulnerability caused by OTPs exposed in the browserβs Network tab
By Shadharanyogeshwaran
3 min read
Introduction
Security in low-code platforms like Mendix is often taken for granted β after all, the platform handles a lot of the heavy lifting. But recently, while working on a login flow with OTP-based authentication, I stumbled upon a critical vulnerability: the generated OTP was fully visible in the browser's Network tab.
This meant that anyone with basic browser knowledge could intercept the OTP, leading to a potential account takeover. In this article, I'll walk you through the vulnerability, why it happened, and the clean architectural fix I applied.
The Setup: Sign In with OTP
Our application offered a "Sign In with OTP" option. The flow worked like this:
- The user clicks Sign In with OTP and enters their username.
- They click Generate OTP.
- A Nanoflow is triggered that:
- Generates a random OTP.
- Stores the OTP in a Non-Persistent Entity.
- Also stores it in the User table (as a non-persistent attribute).
- Commits the data (both persistent and non-persistent commit.
-
The OTP is sent to the user (e.g., via email or SMS).
-
The user enters the OTP to complete the login.
Sounds reasonable, right? But there was a critical flaw hiding in plain sight.
The Vulnerability: OTP Exposed in the Network Tab
Here's the problem: Non-Persistent Entities in Mendix are handled on the client side. When a Nanoflow creates or modifies a non-persistent object, that data is sent back to the browser as part of the response payload.
This means:
π¨ The generated OTP was visible in the browser's Network tab in the Nanoflow response.
Anyone could:
- Open DevTools β Network tab.
- Trigger the "Generate OTP" action.
- Inspect the response payload.
- Read the OTP in plain text.
This is a textbook account takeover vulnerability. An attacker who knows (or guesses) a username could generate an OTP and read it directly from the network response β completely bypassing the email/SMS delivery channel.
Why This Happens
When you store sensitive data in a Non-Persistent Entity or in a Persistent Entity that the user has read access to, Mendix will include that data in the response sent back to the browser. The OTP, being stored in both a non-persistent entity and on the user object (which the user naturally has access to), was fully exposed.
The Fix: Server-Side OTP Storage with Restricted Access
The solution was straightforward but required a change in architecture:
Step 1: Create a Dedicated OTP Entity (Persistent)
I created a new persistent entity called OTP with the following attributes:
OTPCode(String) β the generated OTP valueGeneratedAt(DateTime) β timestamp for expiry logicIsUsed(Boolean) β to prevent OTP reuse
This entity is associated with the User entity (e.g., User_OTP β a one-to-one or one-to-many association).
Step 2: Remove All Client-Side Access
This is the critical step. In the Entity Access settings for the OTP entity:
- β No Read access for any user role
- β No Write access for any user role
By removing read and write access, the OTP entity becomes completely invisible to the client. Mendix will never include it in any response sent to the browser.
Step 3: Update the Nanoflow
The updated Nanoflow now:
- Generates the OTP.
- Creates an
OTPobject and associates it with the user. - Commits the OTP object (persistent commit).
- Sends the OTP via the delivery channel (email/SMS).
- Does NOT store the OTP in any non-persistent entity or on the user object.
Step 4: Validate the OTP Server-Side
For OTP validation, I used a Microflow (not a Nanoflow) that:
- Retrieves the
OTPrecord associated with the user. - Compares the entered OTP with the stored one.
- Checks expiry and usage status.
- Marks the OTP as used upon successful validation.
Since Microflows execute entirely on the server, the OTP value never touches the client.
Key Takeaways
1. Never Store Sensitive Data in Non-Persistent Entities
Non-persistent entities exist in the browser's memory. Anything you put there is visible to the user via DevTools. Treat non-persistent entities as public data.
2. Entity Access Rules Are Your Security Boundary
In Mendix, entity access rules determine what data the client can see. If a user role has read access to an entity, that data can appear in network responses. Remove access for sensitive entities.
3. Use Microflows for Sensitive Operations
Nanoflows run partially on the client. For anything involving sensitive data β OTPs, tokens, passwords, secrets β always use Microflows, which execute entirely on the server.
4. Always Check the Network Tab
During development and testing, regularly inspect the Network tab in your browser's DevTools. If you can see sensitive data there, so can an attacker.
A Note on Mendix Security
Mendix provides powerful security features, but they require conscious application. The platform won't prevent you from exposing data through non-persistent entities β that's by design, since non-persistent entities are meant for client-side UI state.
The responsibility lies with the developer to ensure that sensitive data never leaves the server unless absolutely necessary.
Conclusion
What started as a routine OTP implementation turned out to harbor a critical security vulnerability. By simply moving the OTP storage from a non-persistent entity to a restricted persistent entity and validating it server-side, we completely eliminated the exposure.
If you're building OTP-based authentication (or handling any sensitive data) in Mendix, take a moment to open your browser's Network tab and check what's being sent to the client. You might be surprised.
Stay secure. Stay curious. π
Have you encountered similar security issues in Mendix? I'd love to hear about your experiences in the comments.
Tags: #Mendix #Security #Low-Code #Web Security #Authentication