Case study
helloim.ai: closing a database an export shipped wide open
helloim.ai is vDesktop, LLC’s own product. It runs on a site exported from a hosted page-builder, and the export did not arrive safe: its database answered strangers, its admin panel would let one in, and one of its features would fetch whatever address it was handed. This is what was found before any of it shipped, and what it was rebuilt on.
- Product
- helloim.ai
- Origin
- Exported from a hosted page-builder
- Data
- Moved to our own Cloudflare Worker and KV
- Access
- Closed and verified
The problem
helloim.ai gives a visitor an AI assistant with a voice and a face they choose — tried for free, then licensed for real use against their own AI account. That is the whole product, and none of what follows is about it directly: it is about the site the product runs on, which was exported from a hosted page-builder rather than written by us.
Reviewing code we did not write is a different job from reviewing our own: nothing in an export can be assumed correct because it looks finished, and the finish is the part a builder like this is good at.
So the export was read as a stranger would read it — for what it talks to, what it trusts, and what a visitor could make it do. Three things mattered. Its database answered an unauthenticated caller on every table. Its admin check let anyone in who bothered to sign up. And one of its features would fetch whatever address a visitor handed it, from a server that had no business reaching some of those addresses at all.
All three happened. Each one is below.
The approach
The export’s own database held a key that let the browser talk to it directly, and that same key was what three of the export’s own demo features ran on. Deleting the key came first, before anything else — it closed the exposed writes and all three features in one move, rather than patching each one inside code we would only have to re-patch the next time the export was regenerated.
What replaced it is not a fix applied to the export. It is a server we control, holding its own storage, that the export’s front end now calls instead of the builder’s database. Nothing about a hosted export’s own security model was trusted going in, and nothing about it needed to be trusted afterward — the browser no longer holds a credential that can write anything. How that review runs, in more detail →
A paywall with no wall
A licence table anyone could write to is not a data leak. It is a paywall with no wall.
The exported bundle talked to the builder’s hosted database straight from the browser, with an access key sitting in plain sight in the JavaScript. That key is supposed to be public — the tool’s own security model puts the real gate in row-level policies configured on the project, and this project had none written. So every table answered an unauthenticated caller with an ordinary success: licences, licence activations, contact records, marketing campaigns, social posts, and analytics events, all of it.
The test that proved it was written to prove exactly that without changing anything: an insert naming a column that does not exist. A blocked write answers 401 or 403. This one answered 400 — a schema error. The database had checked the shape of the write and never once asked who was sending it.
On the licence tables specifically, that meant a stranger with no account could grant themselves a paid licence, or take one away from someone who had genuinely bought it, without ever signing in.
The whole data layer now lives on infrastructure we hold, behind a server that decides what any given caller may read or write before it touches storage at all. The browser keeps no credential capable of a write, because it no longer talks to the database directly.
Any signed-up visitor was an administrator
The admin panel’s gate was a line of code that always let everyone through.
The exported admin check read, in effect: allow this person in if the list of approved admin addresses is empty, or if their address is on it. The list the export shipped with was empty. An empty list makes the first half of that either-or true on its own, regardless of the second — so the check granted admin access to any signed-in account, on a site whose signup was open to the public.
Creating an ordinary account was the only step between a stranger and the customer list, the licence table, and every lead the marketing forms had ever collected.
An administrator role is no longer a value any signup path can produce. It is written by a separate process, run from a credential the public site never holds, and every admin route checks that role as its first statement — before a request body is even read, not somewhere further down where a route added later could forget to.
A setting that could reach anywhere
A feature for connecting your own AI provider took an address from the visitor, and a server fetched it.
The site lets a visitor bring their own AI account instead of using the demo’s. The export’s version of that feature read the provider’s web address straight off the incoming request and called it from its own server. A server that will fetch whatever address a stranger hands it is an open proxy wearing our infrastructure’s reputation — free to be pointed at an internal address, at a cloud metadata endpoint, at anywhere a caller wanted a request to originate from that was not their own.
The fix is a fixed table, not a filter. A visitor now picks a named provider — Anthropic, OpenAI, Google, and a short list of others — and the server resolves that choice to one of a small set of addresses it already knows. A request naming anything else is refused by name, before anything is fetched. Adding a destination later is a one-line change and a deploy, on purpose: a new place this can reach is meant to be a decision someone makes, not a string a visitor supplies.
The honest cost is said here rather than left for someone to discover. The site’s own copy once suggested pointing this feature at “your internal company gateway.” That was never actually possible — a server answering requests on the public internet cannot reach an address that only exists inside somebody’s private network, allowed or not — and the wording has been corrected rather than left to fail on the first person who believed it.
Where it stands
- Data layer
- Moved
- Off the export’s database entirely, onto infrastructure we hold. The original access key is deleted, not hidden.
- Outbound reach
- Closed
- Fixed to a named list of providers. Everything else is refused by name.
helloim.ai’s front end is still the site the export produced — its layout, its pages, the demo a visitor talks to. What changed is everything that front end now talks to: its own server, its own storage, and a fixed set of places it is ever allowed to call out to.
None of the three faults above survive a fresh copy of the export being generated or re-downloaded. The credential that made the database reachable from a browser is gone, and the two features built to replace what it powered do not exist without their own server in front of them.
Bella is an AI assistant. Answers can be wrong.
Answers can be wrong. Please don’t share passwords or anything private.
We’d like to switch on Google Analytics cookies to see how the site is used and to measure our ads. They stay off until you choose. Cloudflare’s cookieless analytics runs either way. See our Privacy Policy.