DocsReports and fixes
What we check
Every check that runs today, what it means and what could happen.
On this page
- In short
- The checks at a glance
- How the checks work
- Checks on your website
- Uptime and speed
- Security certificate
- Outdated encryption on your server
- The switch from http to https
- Security headers
- How strict your Content Security Policy is
- Session cookies
- Sign-ins in shared caches
- Insecure content on secure pages
- Where your forms send passwords
- Search engine listing
- Exposed keys
- Keys and tokens in your links
- Scripts from hijacked domains
- Files your page loads from public CDNs
- Development servers and debug pages
- Published configuration files
- Backups and open folders
- Files with passwords left on your server
- Admin tools open to the internet
- Which websites your API answers
- Links that forward to other websites
- What your GraphQL API reveals
- Database tables readable without signing in
- Sign-up settings
- File storage
- Firebase database readable without signing in
- File lists in your cloud storage
- Libraries on your pages
- The framework version your app runs
- Email in your name
- Domain renewal
- Forgotten subdomains
- Subdomains others can claim
- Checks that read your code
- Who can reach which data
- Where money can leak
- Who your app believes
- How fast someone can guess passwords and codes
- Who can spend your AI budget
- Where request data ends up
- What your AI tools let a prompt do
- Links that change your users' data
- Search commands in your database queries
- How your app finishes sign-in with other services
- Webhooks that do not check who sent them
- Search patterns built from what someone types
- Scheduled jobs anyone can start
- Verification that does not stop the action
- Who chooses a token's verification key
- Request keys in nested object writes
- Uploaded archive paths
- Private data in shared caches
- How your sign-in tokens are signed
- Where your session keys are kept
- How your code sets up the session cookie
- What your endpoints send back
- What your server writes into its logs
- Which websites may use your API as your users
- How your app stores passwords
- How your app encrypts data
- How your app compares signatures
- How your app makes codes and tokens
- Whether your server checks who it talks to
- How your pages handle messages from other websites
- Where your app inserts raw HTML
- Database rules in your migrations
- Database views and functions that skip your rules
- Libraries you install
- Keys committed to your repository
- Keys in the folder your site publishes
- Terraform state in your repository
- Private keys named for the browser
- Build settings that put your environment in the browser
- Next.js settings that switch off a protection
- Firebase security rules
- GitHub Actions pinned to malicious code
- Commands strangers can run in your GitHub Actions
- Deployment and build configuration
- When configuration cannot be evaluated
- Related
In short
- Every report runs 34 checks against what your app shows to any visitor. 84 more read your code, once you connect the app's repository. The report lists them as the questions they answer, and some checks answer more than one.
- Below, each check lists what it looks at, the findings it can report and how serious each one is.
- A check reports what it saw on the day. When it finds nothing, that does not prove the problem cannot exist. What Vallit checks, and what not shows how often the checks find a problem and what they never look at.
The checks at a glance
| What we look at | What the scan shows while it runs |
|---|---|
| Whether your app responds, and how quickly | Checking your app is up |
| Your security certificate and when it expires | Checking your security certificate |
| The protections browsers apply to your pages | Checking security headers |
| Whether session cookies are kept out of reach of scripts | Checking how your cookies are protected |
| Scripts or images loaded over an insecure connection | Checking for insecure content on secure pages |
| Whether search engines are told to list your app | Checking whether search engines are kept out |
| API keys and passwords visible in your app’s code | Reading your app for exposed keys |
| Configuration and source files reachable by anyone | Looking for published configuration files |
| Backups, database exports and open folders | Looking for backups and folders left open |
| Which websites your API answers to | Asking your API who may call it |
| Whether your database tables can be read without signing in | Testing whether your database keeps strangers out |
| Whether strangers can sign up as someone else or without an account | Reading how people sign up to your database |
| Whether your file storage shows its buckets to strangers | Asking your file storage what strangers can see |
| Libraries your pages load that have known security flaws | Checking the libraries your pages load |
| Whether strangers can send email that claims to come from your domain | Checking who may send email in your name |
| Whether your domain registration is close to expiring | Checking when your domain is paid up until |
| Subdomains that point at services which no longer exist | Looking for forgotten subdomains |
| Whether a development server or debug pages answer your visitors | Checking your app is not running in development mode |
| Scripts loaded from domains that were taken over by attackers | Checking where your page’s scripts come from |
| Whether the plain http address sends visitors to the secure one | Checking your http address switches to https |
| Forms that send passwords or card details unencrypted or in the address | Checking where your forms send passwords |
| Keys and sign-in tokens written into the addresses on your page | Looking for keys and tokens in your page’s links |
| Whether your Content Security Policy would stop an injected script | Reading how strict your Content Security Policy is |
| Sign-in cookies on responses a shared cache may hand to the next visitor | Checking caches cannot share sign-ins |
| Whether your GraphQL API describes its whole structure to strangers | Asking your GraphQL API what it reveals |
| Whether your Firebase database can be read without signing in | Testing whether your Firebase database keeps strangers out |
| Whether your server still accepts encryption versions browsers have dropped | Checking your server refuses outdated encryption |
| Library files from other websites that load without a fingerprint check | Checking files your page loads from other websites |
| The framework release your app runs, against its published security flaws | Checking the framework version your app runs |
| Configuration files with passwords or keys that anyone can download | Looking for files with passwords left in your app |
| Database admin panels, configuration pages and status pages open to anyone | Looking for admin tools left open on your server |
| File storage that hands its complete file list to anyone who asks | Asking your file storage whether it lists its files to anyone |
| Subdomains pointing at hosted services where anyone could claim the name | Checking whether anyone can claim your subdomains |
| Links on your site that forward visitors to any address handed to them | Checking whether your address can forward visitors to other websites |
How the checks work
- The checks only read. Every request is a GET, HEAD or OPTIONS request, the kinds that fetch without changing anything.
- They start from the address you gave: the homepage, the scripts it loads and a short list of fixed addresses named below. Two targeted checks also ask the file storage your pages name and your own subdomains.
- Some checks also ask public registries, never your app. The OSV vulnerability database answers for library versions, DNS for your mail records, the domain registry for your renewal date and the certificate logs for your subdomains. They receive a package name and version, or your domain name, and nothing else.
- The targeted checks go beyond what a browser loads: every check from Published configuration files down to Subdomains others can claim. The error page that Development servers and debug pages asks for is a targeted request too.
- The targeted checks run only when whoever started the check confirmed they may check the app. The guardian's daily check runs them from the first day for an app on Watch or Care, and once the app's domain is confirmed for an app on Free.
- A finding of a targeted check shows its place, what it lets a person do and its fix steps only once the app's domain is confirmed. Its title and severity show at once. Reading a report has the detail.
- A check that gets no answer from your homepage or a registry is asked once more after a short pause. Only then does it count as not run. A hold such as bot protection is not asked again.
- Every request has one limit of 10 seconds. It covers looking up the address, waiting for its turn, every redirect and the whole answer.
- The checks that send their own requests read a missing or refused address as an answer. They look for published files, open databases and storage, Supabase sign-in settings and GraphQL. They also test forwarding links, which websites your API answers, the switch from http to https, development servers and search engine blocks. A request that runs out of time, gets a server error or is turned away for asking too often is not one. The check then shows Not run, and what it confirmed before stays in the report. The next check asks again.
- A finding never holds a secret. Keys are recorded by kind and fingerprint, environment files by variable names, database tables by whether a row came back.
- Finding text, check errors and cached code verdicts mask recognized keys and Bearer credentials. Addresses keep their path while their parameter values are hidden.
Checks on your website
Uptime and speed
The check loads your homepage. If it answers, the check asks twice more and uses the middle of the three response times.
- Your app did not respond (Critical): no page came back within the time a browser waits, on the first try or the second.
- Your app answered only on the second try (Low): the first request got no reply, a second one did. This usually means the server was asleep and had to start up.
- Your app answers only without encryption (Critical): the https address was silent, but the plain http address answered.
- Your app returns a server error (Critical): the homepage answered with a server error, a status code from 500 to 599. A code from 400 to 499, such as 404 (not found), is High.
- Your app is slower than visitors will tolerate (High): the middle time was 3 seconds or more. From 6 seconds, the finding reads Your app takes a long time to respond.
- Your app is behind bot protection (Info): a security service answered in place of your app. The checks that read your pages then show Not run and the report reads Partial, because Vallit does not judge the challenge page as if it were your app.
What could happen: visitors meet an error page, or leave before the page appears. The check times the homepage only, from Vallit's servers.
Security certificate
The check opens a secure connection the way a browser does and reads the certificate your address presents.
- Your app is not served over a secure connection (Critical): the address uses http, so nothing between visitor and app is encrypted.
- Browsers do not trust your security certificate (Critical): the certificate is self-signed, issued for another address or missing part of its chain.
- Your security certificate has expired (Critical).
- The certificate expires in under 7 days (Critical) or in 7 to 14 days (High).
What could happen: visitors meet a full-page browser warning before they reach your app, and most turn back. The check reads the expiry date; it cannot see whether automatic renewal is set up.
Outdated encryption on your server
The check opens two secure connections to your server on port 443: one that offers only TLS 1.0, and one that offers only TLS 1.1. Nothing is sent over either, and each closes as soon as the server answers.
- Your server still accepts outdated encryption (Low): the server agreed to TLS 1.0, TLS 1.1 or both. Every current browser has dropped these versions, so no visitor needs them.
A server that refuses both is not reported, and the check runs only for apps on https. What could happen: a visitor on an old device or a compromised network can be held to the weaker version. Security reviews and payment compliance checks (PCI DSS) mark the site as failing.
The switch from http to https
When your app answers on https, the check asks for the same address over plain http once and follows where it leads.
An http address where nothing answers at all serves nothing without encryption, so the check passes. An http address that answers with a server error or turns the check away for asking too often leaves it Not run.
- The http address of your app does not switch to https (Medium): the http address answers with a page of your app and never lands on https. It is Low when your secure site sends Strict-Transport-Security for at least 180 days, because returning visitors' browsers then switch by themselves.
What could happen: whoever types the bare address or follows an old link stays on an unencrypted connection, where a shared network can read what they send. An app served only over http is reported under Security certificate instead.
Security headers
The check reads the headers your homepage is served with: instructions from your server that tell the browser how to treat the page. It reads them only when the homepage is an HTML page. A Content Security Policy counts whether it sits in a header or in a meta tag.
- No Content Security Policy (Medium): browsers are not told which scripts may run. A policy in report-only mode is Low, because browsers do not enforce it.
- Your app can be embedded on any website (Medium): nothing stops another site from loading your app inside an invisible frame.
- Browsers are not told to always use a secure connection (Medium): there is no Strict-Transport-Security header on an https app.
- Browsers may guess file types (Low).
- Full page addresses are shared with other sites (Low): a Referrer-Policy header or meta
tag asks for
unsafe-urlorno-referrer-when-downgrade. Without a Referrer-Policy, browsers share only your domain, so a missing one is not reported.
The two Medium findings about framing and secure connections become High when the page has a password field, a card number field or Stripe's checkout. What could happen: injected code runs unchecked, or a visitor is tricked into clicking something they cannot see. The check confirms each protection is present. How strict your Content Security Policy is judges whether the policy would stop an injected script.
How strict your Content Security Policy is
The check reads every Content Security Policy your homepage enforces, in a header or a meta tag, the way a browser does. It asks whether a script an attacker slipped into the page would run.
- Your Content Security Policy would not stop an injected script (Medium): every enforced
policy lets such a script through. A policy does when it restricts no scripts at all, or allows
'unsafe-inline'without a nonce or hash. It also does when it allows scripts from anywhere (*,https:,http:ordata:) or from a public code host such as unpkg.com or cdn.jsdelivr.net. With'strict-dynamic', those last two do not count.
One strong policy protects the page even beside a weak one, so the finding needs every enforced policy to be weak. What could happen: a script injected through a comment, a profile field or a flawed library runs, reads what users type and acts as them. A missing policy and a report-only one are findings of Security headers. The check reads the homepage only.
Session cookies
The check reads the cookies your homepage sets on a plain visit. Only cookies whose names suggest a login session count, so analytics and consent cookies are left alone. sid counts only as a word of its own, as in connect.sid, so a cookie such as sidebar_state is not taken for a session.
- A session cookie is missing basic protection (Medium): it lacks HttpOnly, or Secure on an https app, so scripts or a network eavesdropper can read it.
- A cookie allows cross-site use without a secure connection (Medium): any cookie marked SameSite=None, which lets it travel with requests from other sites, but without the Secure flag. Modern browsers drop it.
- A session cookie does not say when it may be sent from other sites (Low): no SameSite attribute, so each browser applies its own default.
What could happen: someone copies the session and uses the app as that visitor. The check recognises a session cookie by its name alone. It does not see cookies set after someone signs in.
Sign-ins in shared caches
The check reads the response your homepage comes with. It looks for a sign-in cookie, recognised by its name, set on a response that a shared cache such as a CDN may keep.
- A shared cache may hand one visitor’s sign-in to the next (High): the response sets a
sign-in cookie and either came from a shared cache or allows one to keep it, with
publicors-maxagein its Cache-Control header.
When the cache itself reports a hit, the check saw it happen. Otherwise it goes by the Cache-Control header alone. What could happen: visitors end up signed in as someone else and see their data. A response marked private or no-store is left alone. Cookies set after someone signs in are not seen.
Insecure content on secure pages
The check reads your homepage's HTML for addresses that start with http:// on an https page.
- Your secure page loads code over an insecure connection (High): a script, frame, stylesheet, object or embed comes over plain http. A stylesheet link counts whatever order its attributes are in.
- Some images or media load over an insecure connection (Low): an image, video or audio file comes over plain http.
What could happen: someone on the same network as a visitor swaps that code for their own, and it runs inside your page. The check reads the page as served; it does not see what scripts add later. A tag inside an HTML comment is never loaded, so it is left out.
Where your forms send passwords
The check reads the forms in your homepage's HTML that hold a password field or card fields.
- A form sends passwords or card details without encryption (High): on an https page, the form or one of its buttons sends to an http address.
- A sign-in form puts the password in the page address (Medium): a form with a password field is sent with GET. A form that names an address but no method counts, because browsers then send it with GET.
What could happen: anyone on the network reads the password or card number, or the password lands in browser history and server logs. A form your JavaScript draws later and sends with fetch is not in the served HTML, so the check does not see it.
Search engine listing
The check looks for the three ways a site tells search engines to leave it out. They are an X-Robots-Tag header that says noindex, a robots meta tag that says noindex, and a robots.txt that blocks every crawler.
- Search engines are told not to list this app (Info): nobody finds the app by searching. For a private tool or a staging copy, that is what you want, and nothing needs doing.
Exposed keys
The check reads the homepage and up to 12 scripts it loads, looking for strings shaped like secret keys. It recognises keys and tokens from Stripe, Supabase, AWS, GitHub, OpenAI, Anthropic, Slack and SendGrid, as well as private keys.
It also knows the keys of AI services (Groq, Replicate, Hugging Face, OpenRouter, xAI, Perplexity) and of email and messaging (Resend, Mailgun, Mailchimp, Telegram, Slack and Discord webhooks). Developer and data services count too: npm, GitLab, Linear, Notion, Airtable, Shopify, Sentry, PostHog, Mapbox, Doppler, DigitalOcean, Neon and PlanetScale. So do Supabase access tokens and Google OAuth client secrets. A database address with a password counts when it points at a hosted provider, such as Supabase, Neon, AWS RDS, PlanetScale, Railway, Render, Upstash or MongoDB Atlas.
- The finding reads like "Stripe live secret key is visible in your app's code". Most kinds are Critical. Some are High, such as Stripe restricted and test keys, Slack tokens and SendGrid or Resend keys. Slack and Discord webhooks are Medium.
The finding names the key by its kind, its prefix and its length, never the key itself. Keys that are meant to ship to browsers, such as a Supabase public key, are not reported. Neither are placeholders from setup examples, such as sk_live_xxxxx or a private key block that reads YOUR_PRIVATE_KEY. What could happen: anyone can read the key in their browser's tools and use it, so treat it as already known and replace it. The check matches a key by its shape. For a few formats, such as Mailgun and Telegram, the provider's name must also appear nearby. Scripts that only other pages load are not read.
Keys and tokens in your links
The check reads every address on your homepage and in up to 12 scripts it loads: links, images, frames, forms and redirects. It looks at the query string and at what follows the #.
- A key or token is written into an address on your page (High): a key recognised by its format, or a client or API secret, sits in an address. So does a Google API key sent to a paid Google service such as Gemini. When the addresses hold only passwords or sign-in tokens, it is Medium.
Keys made for browsers, such as a Google Maps key or a pk_ key, are left out, and so are placeholders. Links signed for sharing are left out too: Supabase signed URLs, Firebase Storage downloads, and S3, Google Cloud or Azure signatures. What could happen: addresses end up in browser history, server logs and the referring page other sites receive, and whoever reads one can use the key. The check recognises a credential by its name and value alone. The evidence masks it.
Scripts from hijacked domains
The check reads the addresses your homepage runs code from: its script tags, and the addresses that inline scripts and up to 12 loaded scripts add. It compares them with a fixed list of domains that attackers took over, such as polyfill.io and the related bootcdn, bootcss and staticfile.
- Your app loads code from a domain taken over by attackers (Critical).
What could happen: whoever controls the domain decides what runs on your pages, from redirects to scam sites to stolen form input. Only domains on the list count. The polyfill mirrors of Cloudflare and Fastly live on other domains and are not reported.
Files your page loads from public CDNs
The check reads the script and stylesheet tags in your homepage's HTML. It looks for files from public CDNs whose address names an exact version: cdnjs, jsDelivr, unpkg, code.jquery.com, BootstrapCDN, Google's hosted libraries and DataTables.
- Files from other websites load without a fingerprint check (Low): such a file has no
integrityattribute, so the browser runs whatever that server sends under the address.
An address without a version or with @latest cannot take a fingerprint and is left out. So are services that change their file on purpose, such as Stripe.js, analytics, captchas and maps. What could happen: when the CDN is broken into or the file is swapped, the new code runs on your page with your visitors' sessions. That happened to the sites that used polyfill.io in 2024.
Development servers and debug pages
The check reads your homepage and the first four scripts it loads. It looks for what only a development build ships: the Vite client, a webpack or React hot-reload client, or a Next.js development build. It also looks for the error pages that Django, Laravel, Flask, Rails, Express and Symfony show in debug mode. When the targeted checks may run, it asks for one page that does not exist, /vallit-check-missing-page, to see your app's error page.
- Your app is running on a development server (High).
- Your app shows its internal error pages to visitors (Medium).
What could happen: a development server hands out your source code, and Vite's has had flaws that let anyone read files on the machine, keys included. A debug page shows file paths, code and settings to whoever triggers an error. A page that only mentions Vite or webpack in its text is not reported.
Build the app for production and serve that build, never the development command.
- Find the command your host or server starts the app with. A development server is usually started by npm run dev, vite or next dev.
- Change the build step to your framework’s production build (npm run build) and the start step to the command that serves its output.
- For a static Vite or React app, upload the built dist folder to your host instead of running a server at all.
- Set NODE_ENV=production in your host’s environment settings, and redeploy.
- A development server can hand out files it should not, so replace the keys in any environment file that sat beside it.
package.json Your host runs build, then start. dev is for your own machine only.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}How to see it worked: View the page source of your live app: it loads files with hashed names under /assets/ or /_next/static/, and nothing named @vite/client, @react-refresh or webpack-hmr. The next scan will stop listing it.
Published configuration files
The check asks for /.env, /.env.local, /.env.production and /.git/config. It also follows the source maps of up to 8 of your own scripts, the ones served from your domain or its subdomains. Each hit is confirmed by what the file contains, not by the status code.
- Your environment file is downloadable by anyone (Critical): the file that usually holds API keys and database passwords is served to anyone who asks.
- Your source code repository is exposed (Critical): the hidden
.gitfolder was published, with your code and its history. - Your original source code is published alongside the app (Medium): source maps, files that link your shipped code to the code you wrote, let anyone read the original.
What could happen: whoever asks gets your credentials, or your code with every key that was ever committed to it. The report names the variables in an exposed environment file, never their values. A library's source map on a public CDN shows none of your code and is not reported.
Take the file out of what gets published, then change every credential that was in it.
- Download the file yourself first and keep the list of names in it. That list is your rotation checklist.
- Remove it from whatever is served: out of the public folder, out of the image, out of the deployed directory. Add .env* to .gitignore and to your deploy ignore file so it cannot come back.
- Redeploy, then confirm the address returns 404 rather than the file.
- Rotate every credential that was in it: all of them, including the ones that look harmless. The file was readable by anyone for as long as it was up.
- Add a rule that refuses dotfiles outright, so the next one is never served either.
Keep it out of the repository and out of the deploy Removing it from the current commit does not remove it from the history. Rotate the credentials rather than trying to erase the past.
# Stop tracking it, without deleting your local copy.
printf '.env\n.env.*\n!.env.example\n' >> .gitignore
git rm --cached --ignore-unmatch .env .env.local .env.production
git commit -m "chore: stop publishing environment files"
# Vercel and Netlify read this the same way .gitignore is read.
printf '.env\n.env.*\n' >> .vercelignoreWhere the values should live instead
Vercel Project > Settings > Environment Variables. Set per environment,
and redeploy for the new values to take effect.
Netlify Site configuration > Environment variables.
Cloudflare Workers & Pages > your project > Settings > Variables and Secrets.
Mark them as secrets, not plain text.
VPS A file outside the served directory, readable only by the service
user (chmod 600), or your init system's own environment settings.How to see it worked: curl -s -o /dev/null -w "%{http_code}\n" https://example.com/.env returns 404, and each rotated credential shows a new creation date in its provider dashboard.
Backups and open folders
The check asks for /.git/HEAD, /.DS_Store, /backup.sql, /dump.sql, /db.sql and /backup.zip. It also opens /static/, /uploads/ and /assets/ to see whether they list their files. Your address itself counts too when it answers with a file listing. Each hit is confirmed by its contents.
- A database export is downloadable by anyone (Critical) and A backup archive is downloadable by anyone (Critical).
- Your source code repository is exposed (Critical).
- A folder on your server lists its contents to anyone (Medium).
- macOS folder files reveal what else is on the server (Low).
What could happen: your data, code or configuration is one download away. A backup under any other name is not found.
Files with passwords left on your server
The check asks for files that hold passwords or keys and sometimes get deployed by mistake: /.npmrc, /.git-credentials, /.vscode/sftp.json, /.htpasswd, /id_rsa, /.ssh/id_rsa and /id_ed25519. It also asks for /terraform.tfstate, /docker-compose.yml, /docker-compose.yaml, /compose.yml and /appsettings.json. The rest are copies of the WordPress configuration (/wp-config.php.bak, /wp-config.php~, /wp-config.php.save, /wp-config.php.old, /wp-config.bak) and the version control files /.svn/wc.db and /.hg/requires.
- Files with passwords or keys can be downloaded from your app (Critical): the finding names each file and what kind of file it is.
Each hit is confirmed by what the file holds, such as a token that is written out, a password hash or a key block. A status code alone never counts, and neither does an HTML page. Placeholders such as ${DB_PASSWORD} or changeme do not count. What could happen: whoever downloads the file has what is inside, a login to your package registry, servers, database or cloud account. Nothing from inside the file is kept.
Admin tools open to the internet
The check asks for the addresses where database tools usually sit: /phpmyadmin/, /phpMyAdmin/, /pma/, /adminer.php, /adminer/, /pgadmin4/ and /pgadmin/. It also asks for /phpinfo.php, /info.php, /php_info.php and /server-status. A tool counts only when its own page answers, recognised by its title and its own content, such as its login form.
- A database admin panel is open to the internet (High): phpMyAdmin, Adminer or pgAdmin shows its login to anyone.
- A page shows your server’s configuration to anyone (High): a
phpinfo()page lists the server's software versions, paths and environment variables, which often include keys. - Your web server’s status page is public (Medium): Apache's server-status page lists every current request, with the visitor's address and the address they asked for.
A tool behind its own extra login answers 401 and is not reported. A page that only mentions phpMyAdmin is not reported either. What could happen: scripts try common and leaked passwords on these panels all day, and whoever gets in can read, change or delete every table. A tool at any other address is not found.
Which websites your API answers
The check finds up to 6 API addresses that your app's own JavaScript names. Paths under /api/ count, and so do full addresses under /api/ or a versioned path such as /v1/ on your domain or its subdomains, such as api.example.com. An address joined from a base kept in a constant, as in API + "/v1/me", counts too.
The check calls each address from a made-up website and reads which websites the answer allows. This setting is called CORS (cross-origin resource sharing).
- Any website can call your API as your signed-in users (High): the route allows whichever website asks and lets it send the visitor's cookies.
- Your API answers requests from any website (Medium): the route allows every website. It is High when the route also accepts changes.
What could happen: a page your signed-in user visits reads or changes their data without them knowing. Routes the code does not name are not tested. A route whose answer carries these settings counts, even with a server error. One that times out, or fails without them, leaves the check Not run. With your code connected, Which websites may use your API as your users reads the same setting in your server code.
Links that forward to other websites
The check looks for the links on your homepage that name where to go next, such as ?next=, ?redirect=, ?returnTo= or ?callbackUrl=, in links and in forms that use GET. For links to /login, /signin, /sign-in, /auth/login or /logout without such a parameter, it tries next, then redirect, then returnTo. It puts the test address https://example.org/vallit-redirect-check in place of the destination and sends at most 12 requests.
- Your address can send visitors on to any other website (Medium): your app answered with a redirect or a meta refresh to example.org.
When your app redirects the test address back to one of your own pages, the check also tries //example.org/… and /\example.org/…. Both pass a check that only asks for a leading slash. Vallit reads where your app would send the visitor and never requests that address. A redirect to your own sign-in page that carries the test address along in its link is fine. So is a redirect to a sign-in provider such as Google with the test address inside its link. A page that only shows the address as text, or forwards with JavaScript, is not reported. What could happen: a link that starts with your address, in an email or a message, lands your visitor on a copy of your sign-in page. Where a sign-in provider trusts your address as a return address, the same link can hand your visitors' sign-in tokens to whoever sent it. With your code connected, Where request data ends up finds redirects to request-chosen addresses in the code as well.
What your GraphQL API reveals
The check looks in your homepage and the scripts it loads for up to 3 GraphQL addresses, such as /graphql, on your domain or its subdomains. It asks each one to describe itself with an introspection query, sent as a GET request.
- Your GraphQL API describes its whole structure to strangers (Low): the API answered with its full schema. It is Medium when the schema includes operations that change data.
What could happen: anyone gets a map of your API, admin operations included, which makes a missing permission check quicker to find. Third-party APIs, such as a Shopify storefront, and Supabase's own GraphQL are left out. The check never sends POST, so an API that answers only POST is not tested.
Database tables readable without signing in
The check looks in the code your homepage loads for a Supabase project and the public key your app ships. It asks up to 10 tables for one row each, using only that public key.
- Database tables can be read by anyone, without signing in (Critical): at least one table returned data. The finding names the tables.
What could happen: anyone can download those tables in full. Some tables are public on purpose, such as a product catalogue. The check only reads; it records whether a row came back, never the row. It covers Supabase only and does not test writing.
Sign-up settings
The check reads the sign-in settings your Supabase project publishes to every browser, with the same public key.
- People can sign up with an email address they do not own (Medium): sign-up is open and a new account works before anyone confirms the address.
- Visitors can get a signed-in session without an account (Low): anonymous sign-in is on, so every rule written for signed-in users lets these visitors in too.
What could happen: someone opens an account in a customer's name, or passes a rule meant for real customers. Nothing is signed up during the check.
File storage
The check asks your Supabase storage for its list of buckets, with only the public key. By default the answer is an empty list.
- Anyone can list your file storage buckets (Low): the list came back. The finding names the buckets and which of them are public.
What could happen: every file in a public bucket can be downloaded by anyone who has or guesses its link. The check does not list the files inside a bucket, because that needs a kind of request Vallit never sends. Who may change or delete files is read from your migrations, in Database rules in your migrations.
Firebase database readable without signing in
The check looks in your homepage and the scripts it loads for a Firebase configuration. Without signing in, it reads the top-level keys of the Realtime Database named there, never the data under them. For Firestore, it asks for one document from each of up to 6 collections your code names.
- Anyone can read your Firebase database (Critical): the Realtime Database or a Firestore collection answered.
What could happen: anyone downloads your users' records, messages or orders with a single request. Nothing that comes back is stored; the finding counts what answered. Collections your code does not name are not tried, and writing is not tested. Firebase security rules reads the rules committed to your repository.
File lists in your cloud storage
The check reads your homepage and up to 12 scripts it loads for the storage buckets your app takes files from. It knows Amazon S3, Google Cloud Storage, Azure Blob Storage and Firebase Storage. For up to 6 buckets, it asks the provider for the bucket's file list, one entry long, without any key.
- Anyone can list the files in your storage (Medium): the list came back. The finding names the buckets, never a file.
Serving a file from a bucket is normal and is not reported. A refusal such as AccessDenied is the safe answer. What could happen: uploads, exports and backups nobody linked to can be listed and downloaded one by one, your customers' files included. Buckets your pages do not name are not tried. For Supabase, see File storage.
Libraries on your pages
The check reads the header comment that libraries put at the top of their files, and looks that exact version up in the OSV vulnerability database. It knows jQuery, jQuery UI, Bootstrap, AngularJS, Moment, Handlebars, DOMPurify and Vue 2.
- Your app ships libraries with known security flaws (Medium or High): a version your pages load has a published advisory rated medium or higher. The finding names the version that fixes it.
What could happen: attackers read the same advisories. A version is only taken from the library's own header, never guessed from a file name, so most bundled apps have nothing to report here. Libraries you install covers the rest.
The framework version your app runs
The check reads the version that Next.js, React and Angular write into the code they ship. Next.js names it in its main script, React DOM in its own code and Angular in an ng-version attribute on the page. The check reads the homepage and up to 30 scripts, then looks that exact version up in the OSV vulnerability database.
- Your app runs a framework version with known security flaws (Medium or High): an advisory rated medium or higher applies to that version.
A version is never guessed from a file name or a file's size. Canary and other pre-release builds are not looked up, and React inside a Next.js app is judged by the Next.js version. What could happen: attackers read the same advisories, and they can read your version from your code the same way the check does.
Email in your name
The check reads the SPF and DMARC records of your domain, the two DNS records that decide whether mail claiming to come from you is trusted. It only looks when the domain receives or sends email.
- Your domain does not say which servers may send its email (Medium): no SPF record.
- Your domain has more than one SPF record (Medium): mail servers treat two as broken.
- Your domain allows any server to send email in its name (High): the SPF record ends in +all.
- Nobody is told to reject email that fakes your domain (Medium): no single DMARC record.
- Email that fakes your domain is still delivered (Low): the DMARC policy is p=none.
What could happen: a fake invoice or password reset in your name reaches your customers. Apps on a shared address such as your-app.vercel.app are skipped, because those records belong to the platform.
Domain renewal
The check reads your domain's record at its registry, and the date the registration runs out.
- A finding that counts the days left, such as "Your domain registration ends in 12 days" (Medium, High within 14 days). It appears from 30 days before the date.
- Your domain registration has run out (Critical): the domain is in the registrar's grace period.
What could happen: your app and your email stop working, and later someone else can buy the name. Some registries, such as the one for .de, publish no date. For those domains there is nothing to report.
Forgotten subdomains
The check takes the names of your subdomains from the public certificate logs, then uses DNS only. It reports a subdomain whose DNS entry points at a cloud service name that no longer exists. It only counts services such as Azure and AWS Elastic Beanstalk, which hand a free name to whoever asks for it next.
- A subdomain points at a cloud service that no longer exists (High): the finding names the subdomain and where it points.
What could happen: someone claims that name and serves their own pages on your subdomain. The check never sends a request to your subdomains.
Subdomains others can claim
This check covers the services that Forgotten subdomains cannot: their names still resolve in DNS after the site is gone, so only the service's own answer tells. The check takes up to 60 subdomain names from the public certificate logs and follows their DNS entries. For up to 10 that point at a listed service, it sends one GET request over plain http, because an unclaimed name has no certificate.
- Anyone can claim one of your subdomains (High): the service answered with its own "no such site" page for that name. The finding names the subdomain, where it points and the service.
The listed services are Amazon S3, Surge, Help Scout Docs, Helpjuice, Hatena Blog, Strikingly and Canny. Services that check who owns a domain, or that can be claimed only in some setups, are left out. Among them are GitHub Pages, Netlify, Vercel, Heroku, Shopify and Webflow. Services where claiming stopped working are left out too, and so are services whose "no such site" page is too plain to recognise. What could happen: whoever claims the name publishes any page on your subdomain, such as a copy of your sign-in page, under your domain. Nothing of the answer is kept. An app on a shared address such as your-app.vercel.app is skipped.
Checks that read your code
| What we look at | What the scan shows while it runs |
|---|---|
| Whether strangers or other customers can read or change data they should not | Tracing who can reach which data in your code |
| Whether payments can be faked, underpaid, double-counted or kept after cancelling | Following the money through your checkout and webhooks |
| Whether users can make themselves admins, pose as someone else, or fake sign-in events | Checking how your app knows who someone is |
| Whether strangers can use your AI features on your bill, or pick the model you pay for | Checking who can spend your AI budget |
| Request-controlled destinations in server-side HTTP calls | Tracing where your server can be sent |
| Request data used as a command, executable, or shell argument | Tracing request data into system commands |
| Request values composed into SQL text instead of bound parameters | Tracing request data into database statements |
| Request-controlled paths passed to filesystem operations | Tracing request data into file access |
| Request-controlled destinations passed to redirect responses | Tracing where your app sends visitors |
| Request values run as code by eval, new Function, the vm module or a template engine | Tracing request data into code your server runs |
| Scheduled jobs that change data or spend money for anyone who calls them | Checking who can start your scheduled jobs |
| Sign-in tokens signed with a key written in the code, or checked too loosely | Checking how your sign-in tokens are signed |
| Endpoints that send your server’s keys or error internals to the caller | Checking what your endpoints send back |
| Server code that lets any website make signed-in requests and read the answer | Checking which websites may use your API as your users |
| Passwords stored as plain text or with a hash that is quick to crack | Checking how your app stores passwords |
| Signatures checked in a way that leaks them to a patient attacker | Checking how your app compares signatures |
| Reset links, invite codes and sessions made from values that can be guessed | Checking how your app makes codes and tokens |
| Text from users, your database or an AI model shown as HTML without cleaning | Checking where your app inserts raw HTML |
| AI tools that let text a model reads run commands or reach other people’s data | Checking what your AI tools let a prompt do |
| Sign-in forms and code checks that accept unlimited guesses | Checking how fast someone can guess passwords and codes |
| Outgoing connections that accept any certificate, including a forged one | Checking whether your server checks who it is talking to |
| Encryption with a key in the code, a repeated IV or an outdated method | Checking how your app encrypts data |
| Keys, passwords and session cookies written into your server’s logs | Checking what your server writes into its logs |
| Session cookies signed with a key written in the code | Checking where your session keys are kept |
| Session cookies that scripts can read or that travel without encryption | Checking how your code protects the session cookie |
| Pages that act on messages from any website without checking who sent them | Checking how your pages handle messages from other websites |
| Addresses that change or delete data when a signed-in user simply opens them | Checking which links can change your users’ data |
| MongoDB searches that let a request send a search command instead of a value | Checking your database searches for commands hidden in requests |
| Sign-in and account connections with other services that accept a code from any website | Checking how your app finishes sign-in with other services |
| Webhooks from GitHub, Shopify, Slack and others that act without checking the signature | Checking that your webhooks know who sent them |
| Search boxes and filters that turn what someone types into a pattern your server runs | Checking where request text becomes a search pattern |
| Webhook checks whose failure can still reach a data change | Following failed webhook checks to the data they change |
| Sign-in verification keys chosen by the caller | Tracing who chooses the keys behind sign-in tokens |
| Request keys that can alter shared object prototypes | Tracing request keys through deep object writes |
| Archive entries that can write outside the upload folder | Following uploaded archive names to file writes |
| Private records cached without the user or workspace in the key | Checking whether private cache entries distinguish their owner |
| Whether your database rules let strangers or any user change others’ data | Reading your database rules in the migrations |
| Libraries your app installs that have known flaws or are malicious | Looking up your libraries in the vulnerability database |
| Secret keys and environment files written into your repository | Reading your repository for committed secrets |
| Private keys given a browser prefix, so they are published with your app | Reading which keys your app hands to the browser |
| Firebase rules that let strangers or any user read or change your data | Reading your Firebase security rules |
| Workflows that run a stranger’s code or text with your repository’s rights | Reading your GitHub Actions for commands strangers can run |
| Database views that show rows your table rules are meant to hide | Reading the views your database serves |
| Database functions that skip your access rules for whoever calls them | Reading which database functions anyone can call |
| Database rules that trust a profile field users can edit themselves | Checking what your database rules trust |
| Views or functions that hand your users’ sign-in records to the browser | Checking your users’ sign-in details stay out of reach |
| Storage rules that let people who are not signed in upload files | Checking who can upload files to your storage |
| Image settings that let anyone use your app to resize pictures from any website | Reading which images your app’s image optimizer will fetch |
| SVG images served in a way that lets them run scripts on your site | Reading how your app serves SVG images |
| Settings that let other websites trigger your app’s actions for a visitor | Reading which websites may call your Server Actions |
| Build settings that copy every environment variable into the code visitors download | Reading what your build puts into the browser code |
| Keys, environment files and database dumps in the folder your site publishes | Reading the files your site publishes as they are |
| Terraform state, which holds your infrastructure’s passwords, committed to the repository | Looking for infrastructure state in your repository |
| Workflow steps pinned to an action release its maintainers found to be malicious | Checking your GitHub Actions against known malicious releases |
| An explicit request to run a container with elevated host privileges | Reading whether containers request privileged mode |
| An explicit request for the host network instead of a separate container network | Reading whether containers share the host network |
| An explicit request to share the host process namespace | Reading whether containers share host processes |
| An explicit request to share the host communication namespace | Reading whether containers share host memory channels |
| An explicit request for engine API access or a host socket for Docker, containerd or CRI-O | Reading whether containers mount a host engine socket |
| A bind mount of the whole host root into a container | Reading whether containers mount the host filesystem root |
| An explicit addition of SYS_ADMIN or all Linux capabilities | Reading whether containers request broad system administration |
| An explicit unconfined seccomp profile | Reading whether containers explicitly disable system-call filtering |
| An explicit unconfined AppArmor profile | Reading whether containers explicitly disable AppArmor confinement |
| An explicit root user or user ID zero, rather than an inferred image default | Reading whether containers explicitly request the root account |
| Reads unconditional S3 bucket policy grants to a wildcard principal. | Public object reads in cloud storage policies |
| Reads unconditional S3 bucket policy grants allowing public object changes. | Public object changes in cloud storage policies |
| Reads explicitly enabled S3 public canned ACLs without a local ACL block. | Public cloud storage ACL configuration |
| Reads world-wide TCP ingress that includes SSH or Remote Desktop ports. | Cloud administration ingress from every address |
| Reads worldwide TCP ingress to common database and cache service ports. | Cloud database port ingress from every address |
| Reads explicit PubliclyAccessible true on standalone RDS instances and Multi-AZ clusters. | Public address configuration for cloud databases |
| Reads explicit StorageEncrypted false on standalone RDS instances and provisioned clusters. | Cloud database storage encryption configuration |
| Reads explicit BackupRetentionPeriod zero on a standalone RDS instance. | Cloud database automated backup configuration |
| Reads unconditional Action and Resource wildcard grants in IAM identity policies. | Unrestricted cloud identity policy configuration |
| Reads unconditional role trust allowing a wildcard AWS principal to assume the role. | Wildcard cloud role trust configuration |
| Finds GitHub Actions jobs that explicitly request write-all permissions. | Broad workflow permissions |
| Finds literal self-hosted pull_request jobs that check out and execute pull request code. | Pull requests on self-hosted runners |
| Reads the final literal strict-ssl setting in each committed .npmrc. | npm certificate validation |
| Finds final HTTP registry settings in .npmrc, including package scopes, except loopback hosts. | npm registry transport |
| Finds remote actions and reusable workflows without a full commit SHA and Docker steps without an image digest. | Action reproducibility |
| Finds explicit Encrypted=false on CloudFormation EBS volumes. | EBS template encryption |
| Finds explicit TransitEncryptionEnabled=false on CloudFormation ElastiCache replication groups. | Cache transport encryption |
| Finds explicit AtRestEncryptionEnabled=false on CloudFormation ElastiCache replication groups. | Cache storage encryption |
| Finds explicit IsLogging=false on unconditional CloudFormation CloudTrail trails. | CloudTrail recording |
| Finds literal allow-all viewer protocol policies on enabled CloudFormation CloudFront distributions. | CloudFront viewer transport |
These checks are part of a paid plan and run once the app's repository is connected; Connect your code shows how. Without them, the report lists these checks as Not checked and never counts them as passed.
At every check, they read the repository's default branch at one commit, and a finding names the place in your repository. The check keeps the repository's name and that commit's id, not a copy of the code. They come in three kinds:
- The checks from Who can reach which data to Search patterns built from what someone types follow a request or tool call through your JavaScript and TypeScript. Two language models read each finding again. What they reject is left out of the report, and what they could not settle stays in it.
- The rule checks run from Scheduled jobs anyone can start to Where your app inserts raw HTML. Each follows one exact rule at one place in your code, such as a call, a comparison or a response. A rule cannot tell a sign-in code from a button's id, so the same two language models read each of their findings too. What they reject is left out.
- The checks from Database rules in your migrations to Deployment and build configuration read files as they are. These are migrations, lockfiles, rules files, workflows, configuration files, Terraform state and the files your site publishes.
A request can lead into code too large to follow to the end, such as a parser or an interpreter your app runs on what it receives. The checks then read the rest of that route without it, as if that part could have put the request's data into anything it can reach. A problem further along the route is still found that way. A check whose kind of problem that part could hold, such as a database call for Who can reach which data, counts as not finished and the report says so. That includes a database or a function your code hands to that part. The other checks count as read.
Findings of the last kind are read from the files, not seen on the running app.
Before code reaches an AI reviewer, values in recognized credential formats are replaced with markers that preserve source lines and equal-value comparisons. Other code and text remain. The filter does not recognize every secret or remove all personal data. Missing deciding evidence leaves the finding uncertain. A judgment requiring additional code is reviewed again rather than reused from the initial-code cache.
The first kind follows requests that your server answers. A repository with only code for the browser, such as pages, styles and scripts, has none, so these checks have nothing to follow and count as run. Vallit cannot follow a server written in another language, such as Python, or in a framework it does not know yet. For those, these checks show Not run and the report reads Partial. The other two kinds read your files as they are and run as usual.
A route or action that turns itself off in production is left out. Its first line has to leave in production, for example with if (process.env.NODE_ENV === 'production') notFound(). Development tools such as design previews often sit beside real routes this way, and no request in production reaches the code after that line.
Who can reach which data
The check follows every way into your app, such as route handlers, API routes and Server Actions, to each database call. It asks four questions.
| Question | Severity |
|---|---|
| Can anyone change or delete data without signing in? | Critical |
| Can users give themselves more rights? | Critical |
| Can signed-in users reach other customers’ records? | High |
| Does the request get to say which user it is? | High |
Your own team's tools, such as an admin console, may act on every customer's records. The check counts that as a role check when the code compares the signed-in person's own verified address with a list your server holds. That list is an environment variable such as OPERATOR_EMAILS, or a table of your team that names no customer. An address the request sends does not count. Neither does a check whose answer the code never acts on, or a comparison that only shows the session and the user record name the same person.
Row-level security that takes the workspace from the session counts as a check on ownership. When the workspace comes from the request instead, for example in set_config('app.company_id', …), the finding appears under the last question, at the line that sets it.
A webhook from another service, such as GitHub, Shopify or Twilio, is left to Webhooks that do not check who sent them. Both checks count the same routes as webhooks, including a router mounted at /webhooks/shopify. A webhook with no signature check at all is reported there, not again under the first question. When your code checks the signature but a write still runs after the check fails, the write is reported under the first question too.
What could happen: a stranger or another customer reads, changes or deletes data that is not theirs.
Where money can leak
The check follows every checkout, every payment webhook (the address your payment provider calls to report a payment) and every place that grants paid access. It knows the events Stripe, Lemon Squeezy, Paddle and Polar send.
| Question | Severity |
|---|---|
| Can anyone tell your app a payment went through? | Critical |
| Can customers choose what they pay? | Critical when the amount comes from the browser, Medium when a price id does |
| Can paid features be switched on without paying? | Critical |
| Does a repeated payment event credit twice? | High |
| Do customers who cancel keep paid access? | High |
| Will real payment events fail their verification? | High |
What could happen: people get paid features for free or for less, or real payments fail to count.
Who your app believes
The check asks whether your app knows who someone is, or only believes what they say. Besides routes and Server Actions, it reads Server Component pages, middleware.ts, tRPC procedure middleware and the sign-up functions in your Supabase migrations.
| Question | Severity |
|---|---|
| Can users make themselves admins or give themselves a paid plan? | Critical when the data behind the check is read without row-level security, otherwise High |
| Can someone pose as another user with a made-up cookie or token? | Critical |
| Can anyone send your app fake sign-in events? | Critical when the webhook changes or deletes users, otherwise High |
- The first question looks for rights read from profile data every user can change from the browser: Supabase
user_metadataand ClerkunsafeMetadata. A sign-up trigger that copies a role from the sign-up form counts too. Supabaseapp_metadataand ClerkpublicMetadataare fine. - The second looks for a user id taken from
supabase.auth.getSession()in server code, or from a token your code decodes without checking its signature. It reports the id when a database call uses it without row-level security. In a Supabase Edge Function, it counts only whensupabase/config.tomlturnsverify_jwtoff, because Supabase checks the token first otherwise. An id from such a token that is only stored, and decides nobody's access, is not reported here. - The third looks for Clerk webhooks that write to your database without checking the signature Clerk puts on every event.
What could happen: any user becomes an admin, reads another user's data, or changes and deletes users in your database.
How fast someone can guess passwords and codes
The check looks for routes and Server Actions that check a password or a short code before the caller is signed in. It also follows the authorize function of an Auth.js or NextAuth credentials provider, which receives what the sign-in form sends.
- Passwords and sign-in codes can be guessed without limit (Medium for passwords, High for one-time codes): nothing on the way limits how often someone may try.
A password counts when it is compared with its hash, as with bcrypt.compare or argon2.verify, or in a helper named for it, such as verifyPassword. A code counts when a field such as otp, code or pin is compared with the stored one. A long token does not, because it cannot be guessed by trying.
A rate limiter, captcha or bot check counts as a limit in the handler, a helper it calls, middleware.ts or a middleware mounted in front of the route. So does a lockout kept in the database, such as a failedAttempts or lockedUntil field. A signed-in user confirming their own password is left alone. What could happen: a script tries every six-digit code in minutes, or thousands of common passwords against your users' accounts. A limit set only in your hosting provider's firewall is not in your code, so the check cannot see it.
Who can spend your AI budget
The check follows every call to a paid AI model: the AI SDK, OpenAI, Anthropic, Google, Replicate, fal, or a direct request to a model API.
| Question | Severity |
|---|---|
| Can strangers use your AI features on your bill? | High |
| Can the browser pick which AI model you pay for? | Medium |
- The first question asks whether what a visitor types reaches a model with no sign-in and no rate limit, bot check or captcha on the way. A rate limit in
middleware.ts, or one mounted in front of the route, counts. - The second asks whether the request chooses the model, or how long the answer may be, without a list your server controls.
What could happen: anyone uses your app as a free AI service and runs up your provider bill. A rate limit set only in your hosting provider's firewall is not in your code, so the check cannot see it.
Where request data ends up
Six checks follow what a visitor sends (a form field, a URL, a header) through your code to places where it should never decide the outcome. Each finding names the file and line. The code shows the path, so review the destination and its guards before treating it as exploitable.
| Where the request data arrives | What could happen | Severity |
|---|---|---|
| The address of a request your server makes | A caller uses your server to reach internal services or a server of their own. | High |
| A system command | A caller runs commands with your app's permissions. | High |
| The text of a database query | A caller reads or changes records outside the query you meant to run. | High |
| A file path | A caller reads private files or changes files outside the intended folder. | High |
| Where a redirect sends the visitor | A crafted link on your domain sends visitors to an impersonation page. | Medium |
| Code your server compiles and runs | A caller runs any code with your server's rights, reading your keys and data. | Critical |
The finding for the last row reads A request may run its own code on your server. It counts request data that reaches eval, new Function, setTimeout or setInterval given a string, or Node's vm module. The render and compile functions of template engines such as EJS, Pug, Nunjucks, doT and lodash's template count too. JSON.parse runs no code, and a template written in your code with request values passed in as data is fine.
What your AI tools let a prompt do
The check follows the tools your AI model may call: tools made with the AI SDK, OpenAI Agents, Mastra or LangChain, and the tools of an MCP server. It also follows code that runs the tool calls in a raw OpenAI, Anthropic or Gemini response. A tool's arguments count as outside input, because the model writes them from whatever text it read.
- Text your AI reads can steer its tools into unsafe actions (Critical or High): a tool argument reaches a database query, a file path, an address your server fetches or a redirect. It is Critical when it reaches a system command or code your server runs. A tool that changes or deletes a record by an id the model picked, with nothing tying it to the signed-in user, counts too.
An argument limited to a fixed list, for example with z.enum, is the server's choice and does not count. A fixed destination, a parameterised query or a filter on an owner column set when the tools were built keeps a tool quiet too. What could happen: someone plants a sentence in a chat message, a document or a web page the model reads. The tool then reads other customers' data, reaches internal addresses or runs commands.
Links that change your users' data
The check reads the handlers that answer GET: Next.js GET exports, Express and Hono .get(…) routes and tRPC queries. It follows each one through the helpers it calls to the database.
- A link from another site can change your users’ data (High when it deletes, Medium otherwise): the handler knows the user from their sign-in cookie and then changes stored records the address chooses, or deletes the user's own records.
Browsers send sign-in cookies set with SameSite=Lax along with a link someone follows from another site, and Clerk, Auth.js, Supabase and Better Auth set their session cookies that way. A handler that knows the caller from the Authorization header or an API key header is left out, because no browser sends those by itself. Links that carry their own secret, such as ?token= in an unsubscribe or confirmation link, are left out too. So are sign-in, sign-out and callback routes, webhooks, scheduled jobs and payment return pages. Writes that only record a visit, such as a view counter or lastSeenAt, do not count. Neither does a handler or middleware that checks a CSRF token, Sec-Fetch-Site or Origin first. An app whose own session cookie is SameSite=Strict is left out. Pages Router API routes answer every method, so the check cannot tell their GET from a POST and leaves them out. What could happen: someone puts the address in a link, an email or an image on another page. Each signed-in user who opens it changes or deletes their own data without noticing.
Search commands in your database queries
The check follows request data into MongoDB and Mongoose queries: find, findOne, findOneAndUpdate, updateOne, deleteOne, their Many forms and countDocuments.
- Someone can sign in without knowing the password (Critical): a password, code, token or key field is matched with a value from the request.
- Requests can widen your database searches to other people’s records (High): another field is matched with a value from the request, or the whole request is the search.
A value counts when it comes from a JSON body (await req.json(), Express req.body, Hono c.req.json()) or from Express 4's req.query. There it can be an object such as {"$ne": null}, which MongoDB reads as a search command instead of a value. Values made text first are fine: String(x), a template string, a validation schema whose field is a string, or a typeof check that stops the request. So are values from sources that are always text, such as searchParams.get and Express 5's req.query, values wrapped in $eq, and apps that use express-mongo-sanitize, mongo-sanitize or Mongoose's sanitizeFilter. A wider search is left out when the query is also held to the signed-in user, or when the collection has no field that says whose a record is. So is a sign-in that checks the password against its hash afterwards. Signed webhooks are left out. What could happen: one request signs in as one of your users, or reads other customers' records.
How your app finishes sign-in with other services
The check reads the routes where a "Sign in with …" or "Connect your … account" flow comes back to your app with ?code=…&state=…. It looks for a route that exchanges the code for tokens itself, with a request to the service's token address or with googleapis, arctic, simple-oauth2 or openid-client.
- Someone else’s account can be connected to your users’ accounts (High): the tokens or the other service's account are saved to the signed-in user.
- Another site can sign your users in to an account it controls (Medium): the route signs the visitor in with the result.
A route is fine when it compares the state in the address with the one it keeps in a cookie or the session, and stops when they differ. So is a route that sends a PKCE code verifier it stored for this browser. Sign-in run by Auth.js, Clerk, Supabase, Better Auth, Kinde, WorkOS, Auth0, Firebase or Passport keeps its own state and is left out. What could happen: another site sends your signed-in user to your callback with a code from its own account. Your app then connects the stranger's Google Drive, GitHub or Slack to your user, or signs your user in to the stranger's account, where they type in their own data.
Webhooks that do not check who sent them
The check reads the routes that receive webhooks from other services, such as GitHub, Shopify, Slack, Resend, Twilio, Linear, Sanity, Typeform, Calendly and Discord. A route counts when its address or file is named for webhooks or hooks, or when it reads a header only such a service sends, such as x-github-event.
- Anyone can send your app fake webhook events (High): the route changes stored records or sends email or messages, and nothing checks the signature the service sends with each delivery.
A route is also reported when it calls its own signature check and goes on after it fails. The result is ignored, or the check throws and the error is caught. The write is then as open as with no check, and Verification that does not stop the action names the place.
A route is fine when it checks the signature before it acts, with an HMAC of the raw body compared with the header. A timestamp in front of the body, as Paddle and Slack sign it, counts as the raw body. A route whose signature check comes from a package your repository does not contain is left alone, because that check cannot be read. The service's own library counts too, such as @octokit/webhooks, Shopify's webhooks.validate, Slack Bolt, svix or Resend verify, or twilio.validateRequest. A key from your environment compared with a header or the address counts as well. Payment webhooks from Stripe, Lemon Squeezy, Paddle and Polar belong to Where money can leak, and Clerk's to Who your app believes. A route that only stores the raw event in a log table, or only answers a service's verification challenge, is left out. What could happen: someone who finds the address sends made-up events that change or delete your records, or make your app send emails and messages.
Search patterns built from what someone types
The check follows request text into regular expressions: new RegExp(…), RegExp(…), .match(…) and .search(…) with a string, and MongoDB's $regex.
- One request can stall your server with a search pattern (Medium): text from the request becomes a pattern without being escaped first.
Text escaped first is fine: escapeRegExp from lodash or your own code, escape-string-regexp, RegExp.escape, or text stripped to letters and digits. So are patterns picked from a fixed list, patterns compiled with RE2, and plain string methods such as includes, replace and split with a string. Code that runs in the browser is left out, because it only slows down the visitor's own tab. What could happen: a pattern such as (a+)+$ takes seconds or minutes to check against a short text. A few such requests keep your server or database busy, and pages time out for everyone until they give up.
Scheduled jobs anyone can start
The check reads the jobs your repository schedules: the paths under crons in vercel.json or vercel.ts, and routes under a /cron/ path. It reports a job that changes data, sends email or messages, makes changes in Stripe or calls an AI model without checking a secret first.
- Anyone can start your scheduled jobs (High).
A secret such as CRON_SECRET, checked in the handler, in a guard function it calls or in middleware.ts, counts as protection. The x-vercel-cron header and the vercel-cron user agent do not, because any caller can send them. What could happen: someone runs the job as often as they like, sends your mailing list the same email again and runs up your costs. A job scheduled outside the repository, at an address outside /cron/, is not seen.
Verification that does not stop the action
The code check follows webhook signature checks to database writes, message deliveries and paid AI calls.
It reports ignored boolean results, verification promises used before they settle, swallowed errors and actions inside finally.
An insert is still an action when its table is named events or deliveries.
- Your webhook can act even when verification fails (High): an action is reachable without successful verification.
Reject false results and verification errors before changing data or sending messages. A read-only probe is outside this rule. A verifier from a package your repository does not contain cannot be read, so a route that relies on one is not reported here.
Who chooses a token's verification key
The code check traces request data and decoded token headers into JWT verification keys. It follows JOSE key conversions and local key resolvers, including asynchronous resolvers.
- The caller can choose a key used to verify their token (High): the caller controls key material or a JWKS address.
Choose trust anchors in server configuration.
A kid selecting a key from a fixed local map or a pinned JWKS provider does not trigger this rule.
Successful signature verification against an attacker's key does not establish the token's identity.
Request keys in nested object writes
The code check follows caller-controlled property keys through recursive merges and nested writes.
It checks earlier path segments even when the final field is fixed.
A loop that walks an object by request keys, such as cursor = cursor[segment], counts too: the write at the end of the walk is reported.
Guards must reject the same lexical variable used by the write; a different variable with the same name does not count.
It also follows request values written through __proto__, constructor.prototype or the global Object.prototype.
- Request keys can reach a shared object prototype (High): the write can traverse inherited properties without blocking prototype paths.
Reject __proto__, constructor and prototype at every path segment before traversal.
Ordinary own fields and shallow object spreads do not trigger this rule.
Dynamic code and unrecognised merge libraries remain outside its supported patterns.
Uploaded archive paths
The code check follows uploaded AdmZip and JSZip entry names to file writes. Normalizing a pathname alone does not keep it inside the destination. A prefix comparison that also accepts a sibling directory remains unsafe. For operations with two file paths, the check attributes archive-write risk to the unsafe destination. A contained archive destination does not turn an unrelated unsafe source path into an archive finding. An unsafe source path or a read-only open can still produce a general file-path finding.
- An uploaded archive can write outside its destination (High): a traced entry pathname reaches a write without canonical containment.
The same write also counts as a file path the caller chose, under Where request data ends up. A folder created for the entry is not reported on its own; the file written into it is.
Resolve each pathname beneath a fixed root and reject paths outside that root. This rule checks lexical path containment; it does not prove symlink safety or cover every extraction API. Reject archive links and use a fresh destination without existing symlinks.
Private data in shared caches
The code check reads Next.js unstable_cache callbacks that query stored data using a captured sign-in identity.
It checks whether the full captured identity appears in cache arguments or direct key parts.
An unchanged const alias of the complete identity also counts. Mutable aliases and lossy transforms do not prove partitioning.
The identity can come from your own helper, such as requireUser(), when that helper returns the signed-in user.
- A shared cache can return one user’s private data to another (High): the persistent cache does not distinguish the captured owner.
A display name, an identity prefix or a coarse identity bucket does not distinguish users.
Authorize before cache access and include the complete user or workspace scope.
Public reads and React's request-local cache do not trigger this rule.
Other persistent cache frameworks remain outside this rule.
How your sign-in tokens are signed
The check reads where your code signs and checks sign-in tokens (JWTs) with jsonwebtoken, jose or Hono.
- Anyone who reads your code can sign in as any user (Critical): the signing key is written
into the code, or falls back to a value written there when the environment variable is missing, as
in
process.env.JWT_SECRET || 'dev-secret'. Code that accepts unsigned tokens (thenonealgorithm) counts too. - Your app accepts sign-in tokens after they expire (Medium): the token check is told to
ignore expiry with
ignoreExpiration: true.
What could happen: whoever knows the key makes a valid token for any account, an admin's included, without a password. A key read only from your environment is not reported, and the check never sees its value.
Where your session keys are kept
The check reads the key your session library signs its cookies with. It knows express-session, cookie-session, cookie-parser, iron-session, Auth.js and NextAuth, Hono's signed cookies and better-auth. Fastify's cookie and session plugins, Koa's app.keys, and Remix and React Router cookie sessions count too.
- Anyone who reads your code can make a session for any of your users (Critical): the key is written into the code, or falls back to a value written there. The library keeps the whole session in the signed cookie, as cookie-session, iron-session, Auth.js and signed cookies do.
- The key that signs your session cookies is written into your code (High): the same, in a
library that signs only a random id kept on the server, such as express-session, better-auth or
@fastify/session.
A key read only from your environment is not reported, and neither is an empty fallback, which the library refuses. A value used only in development, as in isDev ? 'dev' : process.env.SESSION_SECRET, is fine. So is the key of a cookie that only remembers a preference, such as the colour theme or the language, because a forged one grants nothing. What could happen: whoever knows the key, from a copy of the code or a guess at a default such as keyboard cat, makes a session cookie for any account.
How your code sets up the session cookie
The check reads the options your code gives the cookie that keeps users signed in. It knows express-session, cookie-session, koa-session, Fastify's session plugins, iron-session, Remix and React Router sessions, Auth.js and NextAuth, Lucia and better-auth. A cookie your code sets itself counts when its name says it holds a session.
- Scripts on your pages can read your users’ session cookie (Low): the cookie is set with
httpOnly: false. - Your session cookie can be sent over unencrypted connections (Low): the cookie is set
with
secure: false, or Auth.js withuseSecureCookies: false.
Only a value written as false counts, so secure: process.env.NODE_ENV === 'production' is fine. Cookies that hold no session, such as theme, consent or CSRF cookies, are left out, and so are Supabase's own cookies. What could happen: one injected script copies the session, or someone on the same network reads it from a plain http request. Session cookies reads the cookies your live homepage sets, while this check also sees the ones set after someone signs in.
What your endpoints send back
The check reads what your code sends back: Response.json, NextResponse.json, res.json, res.send, Hono's c.json, new Response(…) and what a Server Action returns.
- An endpoint sends your server’s keys to whoever calls it (Critical): a response is built
from
process.env, or carries secret variables one by one. - Your app sends error internals to the browser (Medium): a response carries an error's stack trace.
Saying whether a key is set is fine, and so are values filtered down to public names and anything inside a development-only branch. What could happen: anyone who finds the address gets your database password and your service keys in one request.
What your server writes into its logs
The check reads what your server code hands to a logging call: console, a logger or log object, pino, winston, Nest's Logger or req.log.
- Your server writes users’ passwords into its logs (Medium): a password from a form, a request body or a variable named for it.
- Your server writes secret keys into its logs (Medium): an environment variable named as a
key, password or token, such as
STRIPE_SECRET_KEYorDATABASE_URL, or the wholeprocess.env. - Your server writes visitors’ sign-in details into its logs (Medium): the
AuthorizationorCookieheader, all of a request's headers or cookies, a session cookie, or an access, refresh or API token.
The report names the most serious kind it found: passwords first, then keys. Logging whether a value is set is fine, and so is a shortened, masked or hashed value. Browser code logs to the visitor's own console and is left out. So is a logger other than console that your app sets up to redact. Example apps, test scripts and commands someone runs in a terminal are left out too, such as a setup command that prints the admin password it just created. What could happen: everyone who reads the logs, in your host's dashboard, a log service or a copied error report, can use the key or sign in as that person.
Which websites may use your API as your users
The check reads the CORS settings in your server code: the cors package, Hono's cors(), and Access-Control-Allow-Origin headers set by hand. It reports code that allows the user's cookies (Access-Control-Allow-Credentials: true) for whatever website asks.
- Any website can use your API as a signed-in user (High).
A list of your own domains that turns other websites away is fine. * together with cookies is not reported, because browsers refuse it. What could happen: a site your signed-in user visits calls your API as them and reads or changes their data. Which websites your API answers tests the same setting on the running app.
How your app stores passwords
The check follows values in fields named for passwords to where your code stores, compares or looks them up.
- Your app keeps users’ passwords readable (Critical): passwords are stored or compared as plain text, or a user is looked up by password.
- Your app stores passwords with a hash that is quick to crack (High): passwords go through MD5 or a SHA hash, or through PBKDF2 with fewer than 10,000 rounds.
Passwords hashed with bcrypt, argon2 or scrypt are fine. Only fields named for passwords count, so a checksum of a file is not a finding. What could happen: one leaked backup gives away every password, and most people use theirs on other sites too. Passwords you hand to a sign-in service never reach your database, so the check does not see them.
How your app encrypts data
The check reads where your code encrypts with Node's crypto, WebCrypto, crypto-js, node-forge or aes-js.
- Anyone who reads your code can decrypt the data your app encrypts (High): the key is
written into the code. A fallback such as
process.env.KEY || 'dev-key'counts too. - Your app encrypts data with a method known to be weak (High or Medium): the same IV for
every message is High, such as a fixed value, zeros, a variable from your environment or a value
taken from the key. ECB mode, DES, 3DES, RC2, RC4, Blowfish and
crypto.createCipherare Medium. - Data your app encrypts can be changed without it noticing (Medium): AES in CBC, CTR, CFB or OFB mode, with no HMAC or authentication tag anywhere in the file.
The report names the most serious problem it found. AES-GCM and ChaCha20-Poly1305 with a random IV are fine, and so are keys read from your environment. Hashing is not encryption and is left to How your app stores passwords. What could happen: whoever has a copy of the code decrypts what your app stored, or someone changes an encrypted value without your app noticing.
How your app compares signatures
The check reads where your code compares a computed signature, such as the HMAC digest of a webhook, with an ordinary comparison such as ===.
- A signature is compared in a way that leaks it bit by bit (Medium).
What could happen: with enough requests, someone times the answers and works out a valid signature for an event they made up, such as a fake payment. A bearer token or cron secret compared the same way is not reported.
How your app makes codes and tokens
The check reads where your code makes a value from Math.random() or Date.now() and keeps it under a name that grants access, such as resetToken, inviteCode, sessionId or otp.
- Codes that grant access can be guessed (High).
The same call for a colour, a delay or a React key grants nothing and is not reported. What could happen: someone who sees a few values, or knows roughly when one was made, works out the next reset link or invite code. A value kept under a name that says nothing about access is not seen. For the common shapes of Math.random() in server code, Vallit can write this fix itself and check it before opening the pull request.
Whether your server checks who it talks to
The check reads the settings of the secure connections your server opens to other services: rejectUnauthorized, strictSSL, checkServerIdentity and NODE_TLS_REJECT_UNAUTHORIZED.
- Your server accepts forged certificates on its outgoing connections (Medium): an HTTP
client, a mail transport or a similar connection accepts any certificate, or
NODE_TLS_REJECT_UNAUTHORIZEDis set to'0'for every connection. - Your database connection accepts any certificate (Low): a database client accepts any
certificate, as with
rejectUnauthorized: falsein itsssloptions. The connection stays encrypted.
Only a value written as false counts; a setting read from your environment is a decision someone made. Development-only branches, tests and tool configuration are left out. So is a setting your app's owner turns on by name for a server of their own, such as "allow self-signed certificates" for their mail server. So are the options of a server your app runs, which concern client certificates, and a raw TLS connection that checks the certificate itself. So is a raw TLS connection that only asks which protocol the other side accepts and closes without sending or reading anything, the way a scanner tests a server. What could happen: someone on the network between your server and the other service poses as it, reads the keys and data your server sends, and changes the answers.
How your pages handle messages from other websites
The check reads the listeners for message events in your browser code: window.addEventListener('message', …), window.onmessage and the window event hooks of usehooks-ts, ahooks and Mantine. It follows each handler through the functions it hands the message to.
- Any website can run code in your pages by sending them a message (High): the handler puts the message into the page's HTML, into code it runs, or into where the page navigates.
- Any website can make your pages send requests it chooses (High): the handler requests an address the message names.
A handler that reads event.origin or event.source is fine. So is one that only keeps the message in state, logs it or compares it. Workers and service workers receive messages from your own app and are left out. What could happen: a site your user visits opens or frames your page and sends it a message that runs its own script there, with your user's session.
Where your app inserts raw HTML
The check reads where your code switches off the escaping of React, Vue or Svelte: dangerouslySetInnerHTML, innerHTML, v-html and {@html}. It reports text from users, your database, another service or an AI model that arrives there without a sanitiser. Markdown turned into HTML by a converter such as marked counts as raw.
- Text from outside is shown as HTML on your pages (High).
A string written in the code, your own content imported at build time and JSON-LD made with JSON.stringify are fine. So is text cleaned by DOMPurify, sanitize-html, xss or rehype-sanitize, also inside a helper in another package of your repository. The check follows the text through React state and your own helpers to see where it came from.
Also fine:
- Code coloured by shiki or sugar-high, which escape the code they colour.
- A NodeBB template that prints values as
{name}, which escapes them. - Your own translation of a key written in the code, a stylesheet bundled with your app, and your environment variables.
- What a visitor types into the page, because it only reaches their own browser.
- CSS your script writes into a
<style>element, and markup kept in a<template>that never reaches the page.
A template that prints a stored value as {{name}} is still reported. So is a <style> that React renders on the server from outside text, because that text can close the tag. What could happen: one crafted comment, profile field or AI answer runs code in every visitor's browser, with their session.
Database rules in your migrations
The check reads the files in supabase/migrations in order and works out which rules your database ends up with.
- Some database tables have no access rules at all (High): a table never gets row-level security switched on.
- Anyone can change or delete data without signing in (High): a rule lets visitors without an account change or delete every row, or every file in a storage bucket.
- Any signed-in user can change or delete everyone’s data (Medium): a rule lets every account change rows or files that are not theirs.
- Your database rules trust a field users can change themselves (High): a rule decides with
user_metadata, directly or through a function it calls. Every signed-in user can rewrite their ownuser_metadatafrom the browser. - Anyone can upload files to your storage (Medium): a rule on
storage.objectslets visitors without an account add files, with no check on who is calling.
Rules that let anyone add a row to a table, such as a waitlist, and rules that let anyone read are not reported, because they are so often intended. Files are different: strangers can fill your storage at your cost. A change made in the Supabase dashboard does not show up in the migrations.
Database views and functions that skip your rules
The check replays supabase/migrations in order and reads the views and security definer functions in the schemas Supabase serves to the browser. These are public and graphql_public, unless supabase/config.toml names others. No database is contacted.
- A database view shows protected rows to anyone (High): a view reads tables with row-level
security but runs with its owner's rights, because it lacks
security_invoker. A materialized view counts too. - A database function skips your access rules for whoever calls it (High when anyone with your public key can call it, Medium when only signed-in users can): the function never asks who is calling, and it changes protected tables, builds its SQL from text or returns rows the rules hide.
- Your users’ sign-in details can be read from the browser (Critical when anyone with your
public key can, High when only signed-in users can): a view or function serves Supabase's
auth.userstable, which holds every account's email address.
A function that checks auth.uid() first, a trigger function and a counter such as views = views + 1 are fine. So is a view over a table anyone may read anyway, and anything the browser's roles are no longer allowed to use. What could happen: someone reads other customers' rows, or every account's email address, around the rules you wrote. For a view that is not materialized, and for a function nothing in your app calls, Vallit can write this fix itself and check it before opening the pull request.
Libraries you install
The check reads the exact version of every library your app runs on from the lockfile (npm, pnpm, Yarn or Bun) and looks it up in the OSV vulnerability database.
- Libraries your app installs have known security flaws (Medium or High): an advisory rated medium or higher applies to the version you install.
- A package your app installs is known to be malicious (Critical): the package was published to harm whoever installs it.
Development tools and libraries without a pinned version are left out. A flaw is reported by its version. Whether your app reaches the flawed part is something a version number cannot tell.
Keys committed to your repository
The check reads every file of the current commit. It looks for keys whose shape names their provider, the same kinds as Exposed keys, and for committed .env files that set secret variables.
- Secret keys are committed to your repository (Critical, High or Medium, after the most serious kind found): the finding names the kind of key, the file and the line.
- An environment file with secrets is committed to your repository (High): the finding names the file and the variables, never their values.
Placeholders, .env.example files, browser variables, the published defaults of local tools and test folders are left out. Only the current commit is read, not the history. A key deleted later still sits in the history, so replace the key first.
Keys in the folder your site publishes
The check reads the folders a web server hands out as they are: public, static, www, wwwroot, public_html and htdocs, also inside up to three other folders, as in apps/web/public. Anyone who asks for the address of a file there gets it.
- Files with secrets are published with your website (Critical): the folder holds an environment file that sets a secret, a private key, a Google service account key or a database dump with what looks like real people's data.
Each kind is confirmed by what the file holds, never by its name alone. Environment templates, placeholder keys, public keys, certificates, sample databases and seed files are left out. Build output such as dist is not read. The finding names the file, the line and the kind, never a value or a row. What could happen: scanners ask every site for names like /.env and /backup.sql all day, and whoever gets the file uses what is inside.
Terraform state in your repository
The check reads committed files ending in .tfstate, including numbered and .backup copies. Terraform keeps the last known value of everything it manages there, in plain text.
- Your Terraform state file is committed to your repository (High when it holds passwords, keys or tokens, Medium otherwise): the finding counts the resources and the values named as passwords, keys or tokens.
A state with no resources, such as the one terraform init leaves, is not reported. Neither is the backend note in .terraform/terraform.tfstate. No value is kept. What could happen: whoever can read the repository has the database passwords, private keys and cloud keys Terraform wrote there. Files with passwords left on your server asks your live app for /terraform.tfstate.
Private keys named for the browser
The check reads variable names, never their values: in env files, .env.example included, in code that reads variables and in configuration files such as vercel.json. It reports a name with a browser prefix, such as NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_ or REACT_APP_, whose rest says it is private.
- A private key is set up to be published to every visitor (High): for example
NEXT_PUBLIC_OPENAI_API_KEY. Names with SECRET, SERVICE_ROLE, PASSWORD or DATABASE_URL count, and so do keys and tokens of paid services such as OpenAI, Anthropic or Resend.
Your framework copies every variable with such a prefix into the JavaScript it sends to visitors. Names made to be public, such as an anon key, a publishable key or a Firebase API key, are left out. What could happen: anyone takes the key from your site and runs up your AI or email bill in your name. A private key under a name that says nothing about it is not seen.
Build settings that put your environment in the browser
The check reads the configuration of your build: Vite, rollup, webpack, Next.js, Astro, Nuxt, Rsbuild, Rspack, CRACO, Vue CLI and Quasar. It looks for Vite's define, webpack's DefinePlugin or rollup's replace set to all of process.env. Such a setting is a common fix for the error "process is not defined" in a Vite app.
- Every server secret is built into the code your visitors download (Critical): for example
process.envdefined asprocess.env, asJSON.stringify(process.env)or as Vite'sloadEnv(mode, root, '').
One variable by name, a filtered copy and loadEnv with its default VITE_ prefix are fine. Builds that never reach a browser, such as webpack's target: 'node' or Vite's build.ssr, are left out. Next.js refuses to build with env: process.env, so that setting is not reported. What could happen: anyone who opens your app reads every key the build could see. Deleting the line later does not take back copies already downloaded.
Next.js settings that switch off a protection
The check parses next.config and reads the settings Next.js receives, also through wrappers such as withSentryConfig. Comments and settings commented out do not count.
- Anyone can use your app to resize images from any website (Medium):
images.remotePatternsallows any host, such as**or*.com, and the image optimizer is on. - SVG images your app serves can run scripts on your site (Medium):
dangerouslyAllowSVGis on, and the images'contentSecurityPolicywas replaced by one that lets scripts run. - Other websites can trigger your app’s actions for a signed-in visitor (Medium):
serverActions.allowedOriginsholds a pattern such as*.com, which matches every.comsite from Next.js 15.3.0. A pattern such as*.vercel.appmatches every Vercel project from 14.1.0.
Each rule follows what the installed Next.js does with the value. A star in images.domains opens nothing, because Next.js compares those hosts one by one. dangerouslyAllowSVG alone keeps the default header that stops scripts. * and ** in allowedOrigins never matched a site. The Next.js version comes from your lockfile or package.json, and without one the third finding is not reported. What could happen: strangers run up your image bill, a crafted SVG runs code on your site, or another site triggers actions as your signed-in visitor.
Firebase security rules
The check reads firestore.rules, storage.rules and database.rules.json as committed.
- Your Firebase rules let strangers into your data (Critical): a rule lets anyone write,
with no condition or
if true, or lets anyone read a catch-all path such as{document=**}or the database root. The test-mode rules the Firebase console starts with count, and so does a catch-all path any signed-in user may write.
A public read of one collection, such as a product list, is a design choice and is not reported. What could happen: anyone reads, changes or deletes your users' data from their own browser. Rules changed only in the Firebase console are not seen. Firebase database readable without signing in tests the running database.
GitHub Actions pinned to malicious code
The check reads the uses: lines of your workflows and of action.yml files. It compares each action pinned to a full commit id with the commits that maintainers named as malicious in four incidents. These hit tj-actions/changed-files, reviewdog/action-setup, xygeni/xygeni-action and aquasecurity/setup-trivy.
- A GitHub Action in your workflows is pinned to malicious code (Critical): the finding names the file, the line and the advisory.
A tag such as v1 is not reported. After each incident the tags were moved back to clean code or deleted, so a tag today runs clean code or fails. A commit id written while a tag pointed at the malware keeps running it, which is why only those commits count. Aqua's trivy-action and Checkmarx's kics and ast actions were hijacked too, but no advisory names their malicious commits, so they are left out. What could happen: each run of that code can read every secret the job has, such as deploy keys, cloud keys and publishing tokens.
Commands strangers can run in your GitHub Actions
The check reads the workflow files in .github/workflows line by line, without running them.
- A stranger can run commands in your GitHub Actions (Critical): a workflow started by
pull_request_target,workflow_runorissue_commentchecks out a pull request's code and then runs something. It is High when text anyone can write, such as an issue title or a pull request body, is pasted into a script through${{}}.
Inputs of a manual run are left alone, because only collaborators can start one. What could happen: a stranger reads your repository secrets, pushes code to your main branch or publishes a release. Only the workflows in this repository are read. For the High kind, where text is pasted into a script, Vallit can write this fix itself and check it before opening the pull request.
Deployment and build configuration
The code-check list above includes explicit settings in container manifests, CloudFormation templates, GitHub Actions workflows and committed .npmrc files.
These checks parse the repository snapshot locally. They call no language model and make no additional network requests.
They do not inspect your cloud account or connect to running containers.
For containers, the checks read standalone compose.yml, compose.yaml or compose.json files and their docker-compose equivalents.
Compose overrides, multiple Compose files in one directory, include and extends need a resolved single-file configuration.
Kubernetes support covers v1 Pods and ReplicationControllers, and apps/v1 Deployments, StatefulSets, DaemonSets and ReplicaSets.
It also covers batch/v1 Jobs and CronJobs, including workloads inside a v1 List.
These are Linux workload checks. Windows containers and Pods are outside their scope.
Helm templates need rendered manifests; custom resources are outside these checks.
The container findings concern requested privileges, host namespaces, engine sockets, host mounts, Linux security profiles and an explicitly requested root user. They respect supported container overrides of Pod security settings. An image with no explicit user setting is not reported as running as root. A read-only socket mount can still allow engine API calls; read-only host files still carry a confidentiality concern. Runtime permissions and admission controls affect what a deployed workload can actually access.
CloudFormation checks read literal settings of supported AWS resource types in YAML or JSON. They cover S3 policies and ACLs, EC2 ingress, IAM policies, RDS settings, EBS encryption, ElastiCache encryption, CloudTrail recording and CloudFront viewer protocols. An RDS backup finding requires an explicit zero retention period on a standalone instance. References, transforms, inheritance or conditions affecting a relevant setting need resolution before that check can finish. Missing properties are not treated as disabled protections. Account defaults and external controls are not inspected. Terraform and CDK source are outside these configuration checks.
The workflow checks read .github/workflows/*.yml and .yaml, without executing a job.
They inspect explicit token permissions, supported pull-request code execution on self-hosted runners, and mutable remote action or reusable-workflow references.
Job permission overrides count. A self-hosted runner label alone does not establish that pull-request code runs there.
Mutable references are a Low finding about reproducibility, including references to GitHub-maintained actions.
They do not mean the referenced code is malicious. Local actions are outside the mutable-reference check.
A setting that appears in many places is one finding. A repository with many mutable action references gives one Low finding that lists the places, and costs the score once. The same holds for every configuration check on this page: one rule, one finding, every place listed.
The npm checks read explicit certificate-validation and registry settings in committed .npmrc files, respecting same-file overrides.
They exclude loopback HTTP registries. User configuration, environment variables and command-line overrides are not evaluated.
Registry addresses and credentials are never recorded in findings.
When configuration cannot be evaluated
A reason starting Not evaluated: means a relevant configuration could not be resolved within the supported scope.
Examples include malformed files, aliases, unresolved settings, conflicting options or a matching file missing from the snapshot.
That check is Not run, rather than passed. Reading a report explains its evidence.
A clear result means no explicit matching setting was found in the supported configuration that was read. It does not establish that the deployment is secure or that every configuration format was covered.
Related
The score, the bands and what each finding shows.
What Vallit checks, and what notHow often the checks find a problem, and what they never look at.
Fixing what we foundFix kits for your stack, and connecting your code.
Confirm your domainAdds the targeted checks to the daily check.
Privacy and safetyWhat Vallit reads, stores and never touches.