Fixing the finding fixes one line. Changing the instruction that produced it fixes every line the next prompt writes, which is the only version of this that scales when the code is arriving faster than it can be read.
highVC010 · 19 of 25 projects · 66 occurrences
Permission checks that live in the browser
Client-Side Only Authorization
Why this workflow produces it
This is the finding that is really about this builder, and it follows directly from how the platform works. A Base44 project talks to a hosted backend through an SDK that runs in the browser, so for most of these projects the entire application is client code. There is no server file in the repository to put a check in.
So a role test — show the admin panel to admins, hide the delete button from a viewer — gets written where the app actually is: inside a React component. As a user-interface decision that is completely correct, and the app behaves properly for every user who is not trying anything.
It stops being a security control the moment someone opens developer tools. The component tree, the role variable and the conditional are all in code that was shipped to the visitor, and the same session that renders the page can call the SDK directly from a console. Hiding a button does not stop the operation behind it.
The control that does stop it lives in the platform's own permission configuration, and that is not in the repository at all. Nothing in the codebase reminds you it exists, and a scan of the codebase cannot confirm you have set it.
The change that prevents it
Treat every role test in the client as presentation, and pair each one with a rule enforced on the data itself in the backend's permission settings. The prompt change is to require the pairing out loud, so the generated component and the backend rule get designed together rather than one of them being assumed.
Paste this into your project instructions, or into the prompt itself:
Treat every role or permission check in React as
presentation only. For each one, name the matching
server-side rule that restricts the same data in the
backend's entity permissions, and set it there. Never let a
hidden component, a disabled button or a client-side route
guard be the only thing keeping a user out of another
user's records.
The scan that verifies it
Scan to get the list, then use it as an inventory: for each hit, name the backend rule that enforces the same restriction. Then check it for real — sign in as your lowest-privilege test user and call the data operation directly from the browser console. If it returns records that user should not see, the check was decorative.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC010")] | length'
Honest caveat. Be clear about what this rule can and cannot tell you here. It reads your repository, and on this platform the enforcement lives outside it — so a project with perfectly configured backend permissions will still report every one of these hits, and a clean result on this check is not really achievable. A hit is not evidence of a hole. What the list is genuinely good for is enumerating every place your app makes an authorization decision, so you can confirm each one has a server-side twin.
highVC016 · 18 of 25 projects · 128 occurrences
Navigation that goes wherever it is told
Unvalidated Redirect
Why this workflow produces it
Read the two numbers in the header of this section together: it reached most of the sample and fired several times inside each project. That is not one login redirect. That is a navigation pattern used throughout the app.
These projects are single-page dashboards, and the generated navigation helper takes its destination from something dynamic — a query parameter, a stored return-to value, a field on a record — and hands it to the router or straight to the browser's location.
The behaviour it implements is genuinely useful, which is why it never gets removed: send the user back where they came from after signing in, or forward to the thing they clicked before they were asked to authenticate. The only missing piece is that the destination is never checked against the set of places you are willing to send somebody.
The result is a link on your own domain that quietly bounces a visitor onto someone else's, carrying your app's credibility with it. On a page people reach while signed in, that is a working phishing setup that costs the attacker nothing to build.
The change that prevents it
Resolve destinations against an allowlist of your own routes instead of passing a URL through. A redirect parameter should name a route, not supply a location.
Paste this into your project instructions, or into the prompt itself:
Never pass a value from a query string, a stored value or a
record field directly to router.push, navigate or
window.location. Redirect targets must be looked up in an
allowlist of internal route names defined in code. If a
value does not match one, fall back to the home route
rather than following it.
The scan that verifies it
Scan, then try it: load your own app with a redirect parameter pointing at an external domain and see whether it follows. That single test answers the question faster than reading every hit.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC016")] | length'
Honest caveat. This check cannot always tell an internal route constant from a value someone else controls, so expect a share of these to be internal navigation that was never a risk. It is also worth being accurate about the impact — in a client-side router this is a phishing and trust problem, not a server-side request forgery. Worth an hour, not a weekend.
mediumVC037 · 13 of 25 projects · 468 occurrences
Error handlers that hand back the internals
Stack Traces Exposed in API Responses
Why this workflow produces it
Read the two numbers in the header of this section together. It reached half the sample and still fired hundreds of times, which is what a copy-pasted shape looks like: once per operation, in the projects that have operations at all.
The generated catch block is almost always the same three lines — catch the error, put its message into the body of the response or straight onto the screen, carry on. It is a completely reasonable thing to write while you are building, and nothing about the app misbehaves afterwards, so it never comes up again.
On this platform a lot of that error text originates in the hosted backend and travels back through the SDK. What surfaces is the backend's own error wording, your entity and field names, and often the operation that failed — a readable map of your data model for anyone who sends a deliberately malformed request and reads the reply.
The change that prevents it
Ask for the error contract explicitly, once, and it applies to every handler generated afterwards. Log the real error where you can read it, show the user a generic message and a correlation id.
Paste this into your project instructions, or into the prompt itself:
Never render or return a raw error message, an error object
or a stack to the user. Show a generic message plus a random
correlation id, and log the full error with that same id so
it can still be traced. Apply this to every catch block you
generate, on the client and on the server.
The scan that verifies it
Scan, then confirm by hand: trigger a request that is guaranteed to fail and read what comes back. It should tell you nothing you did not already know about your data model.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC037")] | length'
highVC020 · 25 of 25 projects · 95 occurrences
The response headers a static host does not set for you
Missing Content Security Policy (CSP)
Why this workflow produces it
This is really three findings in the table above — the missing content-security policy, the missing frame-ancestors control, and the missing content-type and referrer policies. They travel together because they have one cause.
A Base44 project ships as a static bundle served by a host. All of these are response headers, and a static file does not carry a response header. Something in front of it has to add them, and the default deployment target usually adds none.
Nothing looks broken as a result, because none of these headers change how a working app behaves. They only change what happens during an attack, which is why they survive every round of testing you do before launch.
The change that prevents it
Add them where the host reads them: a _headers file on Netlify or Cloudflare Pages, the headers array in vercel.json on Vercel. The content-security policy is the one worth real time and the one most likely to break something, so run it in report-only mode first and read the reports before you enforce it. The rest are close to free.
The scan that verifies it
Scan for all three, then check the live response and confirm the headers actually arrive: curl -sI https://your-app.example. The scan reads your repository; only the response proves a header is really being sent.
npx xploitscan scan . -f json \
| jq '[.findings[] | select(.rule == "VC020" or .rule == "VC027" or .rule == "VC056")] | length'
VC027 and VC056 are outside the free rule set. The free scan runs 30 of the 212 rules, so a free plan never evaluates them. This command also counts VC020, which a free scan does run — so the number it returns on a free plan is the VC020 count alone, and it says nothing either way about VC027 and VC056.
Honest caveat. This cluster fired on every project in the sample, which says more about the scaffold than about any developer — and part of it is a measurement limit on our side. The scanner reads your repository, so a host that already injects these headers still reads as missing. Check the live response before you spend an afternoon on it.