What the decivo Scan checks
Rule catalogue
Every check has a stable, versioned code (e.g. RSC-SEC-014) and its own page: what is checked, why it matters and what to do. The catalogue is the open reference behind every verdict.
Scan your own app for freeHow the verdict is derived
Until August 2026 this section held arithmetic: weighted dimensions, penalty points per finding, thresholds. It has been removed without replacement. A number built from freely chosen weights looked like a measurement but was an opinion with a decimal place — and it could hide an evidenced finding behind a good average. Three readable rules on severity and confidence take its place. The values on this page come straight from the running code, not from documentation that can go stale.
Severity: how bad it would be
Four levels. They state what the finding causes if it holds. Pure notices (“Supabase detected”) count internally as info and enter neither the list nor the verdict.
- Critical
- High
- Medium
- Low
Confidence: how sure we are
Derived from the tier that produced the finding. A missing header is beyond doubt from the outside; a guess from a partially reconstructed source map is not. Without this second value, observation and suspicion would sit side by side as equals:
- confirmed
- high
- medium
The three rules, per dimension
- Stop: at least one finding with severity “critical” AND confidence “confirmed”. Both together — “confirmed” only ever comes from a run with domain proof or repository access. This explicitly means: a purely anonymous external scan never says “stop”. It holds no finding it triggered itself or read in the source.
- Iterate: otherwise — at least one finding of “high” or above, or at least 3 findings of severity “medium” within the same dimension, or a single medium finding in security or privacy. A single medium finding is normal in an MVP; 3 in one area are a pattern. In the two areas that carry trust, however, one is enough: we cannot call a system “solid” next to an anonymously readable table or tracking without consent.
- Go: otherwise. Findings of severity “low” never flip the green light — eight missing HTTP headers are not a broken foundation. And two medium findings are exempt: “Content-Security-Policy missing” and “HSTS header missing” describe an absent extra layer of protection, not a defect. Not hardened is not the same as broken.
The overall verdict is the worst verdict across the six dimensions. Findings are not added up across areas: the threshold is meant to spot a pattern within ONE area. Findings a connected repository has dismissed no longer count.
Whatever could not be assessed is marked “limited” and shows “—”, never a zero and never a flattering top mark. Next to it stands the scan depth in numbers: how many checks applied in your case and how many of them actually ran. Checks that do not apply to you leave the denominator — checks we could not run stay in it.
Security
- No end-to-end HTTPSChecks whether the final response is served encrypted or ends up on plain http.
- HSTS header missingChecks whether Strict-Transport-Security is set so even the first request is protected.
- No Content-Security-PolicyChecks header and meta tag for a Content-Security-Policy as the main XSS mitigation.
- MIME sniffing not disabledChecks whether `X-Content-Type-Options: nosniff` stops content-type guessing.
- No clickjacking protectionChecks X-Frame-Options or CSP `frame-ancestors` against embedding in foreign pages.
- CORS wildcard on the public documentRecords `Access-Control-Allow-Origin: *` on HTML — not a flaw by itself, but a signal to check.
- Server technology disclosedRecords version headers such as `X-Powered-By` that make targeted exploit search easier.
- Cookies without Secure/HttpOnlyChecks issued cookies for the Secure and HttpOnly flags.
- Supabase client configuration visibleRecords project URL and anon key in the bundle — expected, but the starting point for access checks.
- Supabase service_role key in the clientDecodes JWTs in the bundle and reports the `service_role` claim, which bypasses RLS entirely.
- Secret key in shipped codeSearches for real secret patterns (Stripe, AWS, GitHub, PEM) in HTML and bundles.
- Firebase backend detectedRecords Firebase configuration in the client — allowed; what matters are the security rules.
- .env file publicly retrievableRequests known env paths and confirms hits by KEY=VALUE line signatures.
- .git directory publicConfirms a shipped `.git` via the signatures of `config` and `HEAD`.
- Database tables granted to the anon roleQueries the schema endpoint with the client key the app publishes itself and lists the tables granted to the anon role — without fetching a row or a row count. Granted is not the same as readable: whether rows come out depends on Row Level Security.
- Database tables anonymously readable (proven)Answers the actual question: do rows really come out with the publicly shipped key? Counting happens purely via the `Content-Range` header of a HEAD request — not a single row is fetched.
- Google API key in the clientRecords keys in `AIza…` format in shipped code — expected for Maps and Firebase, risky without a domain restriction.
- Database credentials visible in the clientFinds connection strings with embedded username and password in HTML or bundles.
- Firestore collections anonymously readableQueries the collections the app names in its own bundle using the public web key — with `mask.fieldPaths=__name__`, so document names only, not a single field value.
- Database functions callable by anonymous usersLists the RPC functions granted to the anon role, read from the schema description — read, never called.
- Signup open, accounts confirmed instantlyReads the anonymously available auth configuration: self-signup open and email confirmation disabled.
- AI API called directly from the browser (statically proven)Finds AI provider endpoints in shipped code. `high` with a matching key pattern in the same bundle, `medium` without.
- Browser writes directly to the databaseFinds write calls (`insert`, `update`, `upsert`, `delete`, storage upload) in shipped code or in `use client` files.
- Credentials compared in browser codeFinds comparisons such as `password === "…"` against a fixed literal in shipped code.
- Storage buckets publicly listableChecks whether file buckets hand out their object listing anonymously.
- Firebase database anonymously readableQueries the Realtime Database endpoint with `shallow=true`: if it returns key names instead of “permission denied”, the database is open.
- Protected area guarded only in the browserDetects admin/account routes the client router knows while the guard lives purely as a condition in the bundle — anyone with the URL gets in.
- Role check inside the client bundleFinds role comparisons such as `role === "admin"` in shipped JavaScript — trivially manipulated in the browser.
- Access token in localStorageObserves at runtime whether a JWT-like value ends up in web storage — readable via XSS.
- AI API called directly from the browserObserves calls to LLM providers from the client — the required key is exposed and any visitor can generate on your bill.
- Backup or database dump publicProbes typical dump/backup paths and confirms hits via SQL or archive signatures.
- No Permissions-PolicyChecks whether camera, microphone and geolocation are explicitly restricted by header.
- No Referrer-PolicyChecks whether outgoing links leak internal URLs including query strings to third parties.
- Unencrypted assets on an HTTPS page (active / passive)Finds resources embedded via `http://` on an https page and separates active ones (scripts, frames, stylesheets — blocked by the browser) from passive ones (images, audio, video — warning, no block).
- Sign-in without a detectable limitAfter ownership proof, sends a small series of failing sign-in attempts and checks whether anything throttles.
- Admin area reachable anonymouslyRequests discovered admin paths anonymously and reports content instead of a redirect to sign-in.
- No SPF and no DMARC — email spoofing unprotectedChecks whether the domain publishes at least an SPF or a DMARC record so receivers can recognise forged mail.
- No DMARC recordChecks whether an existing SPF record is paired with a DMARC record that says what should happen to forged mail.
- SPF record with `+all`Checks whether the SPF record ends in `+all` (or a bare `all`), which explicitly allows every server to send as the domain.
- DMARC with `p=none`Checks whether the DMARC record is still at `p=none`, telling receivers to deliver forged mail anyway.
- No DKIM key found under common selectorsQueries a handful of common DKIM selectors and reports when none of them holds a key — a hint, not a finding of absence.
- TLS certificate expiredReads the host's certificate and checks whether its validity date has passed.
- TLS certificate about to expireReads the host's certificate and reports when it expires in 14 days or fewer.
- Self-signed TLS certificateChecks whether the served certificate is vouched for by its own subject instead of a public certificate authority.
- No CAA recordChecks whether the domain uses a CAA record to restrict which certificate authorities may issue certificates for it.
- No security.txt — no contact for vulnerability reportsChecks whether `/.well-known/security.txt` serves an RFC 9116 file with a contact line.
- security.txt without or with an expired ExpiresChecks whether the existing security.txt carries the mandatory `Expires:` line and whether that date is still in the future.
- package.json publicly accessibleChecks whether `/package.json` serves the project file with its name and dependency list.
- API description (OpenAPI/Swagger) publicly accessibleChecks the usual paths (`/openapi.json`, `/swagger.json`, `/api-docs`, `/docs` …) for an OpenAPI/Swagger description or a Swagger UI / ReDoc page.
- Third-party scripts without Subresource IntegrityFinds `<script src>` and `<link rel=stylesheet>` from public CDNs (cdnjs, jsDelivr, unpkg, jQuery, Bootstrap …) without an `integrity` attribute.
- Database wide open (composite)Combines what would be explainable in isolation: the access key in shipped code, missing or permissive Row Level Security and — for the critical tier — writes straight from the browser.
- Multiple secret families in the same build (composite)Fires when at least two independent secrets (service-role key, database connection, credentials, provider keys, git history) were found in the same run.
- Phishing-ready: mail spoofing plus login (composite)Combines a domain without SPF/DMARC with a login or contact form on the site — the ingredients of a credible phishing mail in the domain's name.
- Security policy allows scripts directly in the HTMLChecks whether the Content Security Policy allows 'unsafe-inline' in script-src (or default-src) without a nonce or hash.
- Tables without Row Level SecurityCollects every `create table` and every `enable row level security` across all migrations and reports tables that are never protected.
- RLS policy allows everyoneFinds policies with `using (true)` — RLS is on but has no effect.
- RLS policy checks only that someone is signed inFinds policies using `auth.role() = 'authenticated'` or `auth.uid() is not null` without binding to the row owner.
- RLS policy allows arbitrary writesFinds `with check (true)` — the read condition may be strict while writes stay wide open.
- Edge function without JWT verificationFinds `verify_jwt = false` in `supabase/config.toml` — the server function is callable without signing in.
- Firebase rules allow everyoneFinds `allow …: if true` in `.rules` files — Firestore or Storage is wide open.
- Paid server function without access controlFinds edge functions that call an AI model, SMS or email sending without verifying the caller or limiting volume — anyone can trigger costs on your bill.
- service_role key in source codeDecodes JWTs in source files and reports the `service_role` claim; on the syntax tree it also reports client files that read the key from the environment (comments do not count).
- Secret key in source codeSearches source files for real secret patterns (Stripe, AWS, OpenAI, Anthropic, Slack, SendGrid, PEM).
- Database credentials in plaintextFinds connection strings with embedded username and password.
- .env file committedDetects a `.env` with real values in the source tree (not `.env.example`).
- Supabase in use, schema not versionedReports Supabase in the code without migrations containing tables and RLS — access control then cannot be verified from the code.
- Secret exposed via a public env prefixFinds variables with a public bundler prefix (`NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, `PUBLIC_`, `EXPO_PUBLIC_` …) whose name implies a secret — such values are compiled into the client bundle.
- Env file inside the shipped bundleDetects a `.env` with real values among the files reconstructed from source maps — the build shipped it.
- Server Action without an access checkInspects the body of every `"use server"` function that writes to the database and reports a missing identity check — server actions are public endpoints.
- Public write action with abuse protectionFinds server actions and route handlers that write to the database without sign-in but rate-limit calls and validate input first — intentionally public forms.
- System prompt in the shipped codeChecks whether the instruction steering an AI assistant is shipped in the browser code.
- Shipped library with a known vulnerabilityDetects front-end libraries and their version in the served bundle and in recovered source maps (retire.js signatures) and matches the version against OSV or the bundled retire.js database. Reports only on a confidently detected version.
- Dependencies with a known vulnerabilityMatches the repository's package versions against the public OSV database and names the affected packages with advisory IDs.
- Secret key in the Git historyChecks the added lines of recent commits against the same pattern list as the current tree — a deleted key stays readable.
- Env file was in the repository and got removedReports environment files present in the checked history but missing from the current tree — the old values stay retrievable.
- Signed-in strangers read other people's recordsAfter domain verification, creates a throwaway account and counts how many rows that stranger sees in personal-data tables — without fetching a single row.
- Table fully readable by any signed-in userReports tables hidden from anonymous callers but fully visible to a freshly created stranger account — intended for catalogue data, the same flaw otherwise.
- Tenant isolation holds under testConfirms that a freshly created account with no data of its own saw no rows in the tables checked.
- A stranger's account can see your uploaded filesUses a throwaway account to check whether non-public storage buckets can be listed.
- File storage sealed off from stranger accountsThe positive counterpart: no non-public bucket gave a stranger account a file listing.
- A stranger's account may write to your tablesCreates a throwaway account and checks per table whether it may write.
- Write access for stranger accounts rejectedThe positive counterpart: the checked tables rejected every write attempt.
- Cross-tenant check could not runRecords that no account could be created (signup closed or confirmation required) — tenant isolation stays unverified.
- Access check runs after the writeCompares the position of the auth call and the database write within the same function body and reports checks that happen after the write.
- Auth result is never evaluatedReports functions that look up the identity but neither check the result nor bind it to the write — the call has no effect.
- API route without an access checkFinds route handlers with mutating methods that contain no authentication.
- Input reaching the database unvalidatedTraces request bodies that flow into an insert or update without schema validation (mass assignment).
- Webhook without signature verificationDetects payment webhooks that process the payload without verifying the signature — anyone could fake a payment.
- Unsanitised HTML renderedFinds `dangerouslySetInnerHTML` with a dynamic value — the classic XSS path.
Privacy (GDPR)
- Google Fonts loaded at runtimeFinds references to fonts.googleapis.com/gstatic.com that send visitors' IP addresses to Google.
- US trackers embedded in the shipped codeDetects analytics and pixel scripts with third-country transfer (Google Analytics/Tag Manager, Meta Pixel, Hotjar, Segment) in the shipped markup. Whether they load only after consent is not checked by this rule — the browser check does that (RSC-PRV-005).
- Mandatory legal pages not visibleFor targets with a German, Austrian or Swiss connection, checks whether imprint and privacy policy are linked.
- Privacy signals unremarkableStandardised classification when no third-country transfers are visible from outside.
- Trackers fire before consentObserves in the browser which third-party hosts are contacted on first load before anything was agreed to — the provable violation static checks can only suspect.
- Data sent to third-country servicesLists foreign hosts contacted at runtime that typically process outside the EU.
- Trackers without a consent layerReports consent-requiring scripts while no consent mechanism is detectable in the markup.
- Tracking cookies set before consentObserves in the browser which cookies are set before any consent has been given.
- Third-party cookies set before consentReports cookies from foreign domains that were set before any consent was given.
- Consent without an equal reject optionChecks the rendered state for a reject option on the same level as the accept button.
- Legal notice: sample values from the templateChecks whether the German legal notice still contains example values — sample address, placeholder email, unreplaced template fields.
- Testimonial with an invented faceFinds portraits from random-face generators inside sections that show testimonials or references.
- Generated media without a visible noticeFor apps that generate images or audio, checks whether the visible text says anywhere that content comes from an AI.
- Contact data in the shipped JavaScriptFinds e-mail addresses and phone numbers in clear text inside the shipped JavaScript bundles — not in the HTML, where the legal notice requires them.
- List of e-mail addresses public in the markupCounts distinct e-mail addresses in the shipped HTML of the start page and the discovered sub-pages and reports from ten upwards — that looks like a publicly rendered member or customer list.
- CSP violations are reported to a third partyReads `report-uri` and `report-to` from the Content Security Policy (header or meta tag) and reports when the reports go to a foreign host.
- Reject has no effect — trackers still loadClicks the detected “reject” control of the consent banner in the browser and observes whether tracking services are still contacted or tracking cookies still set afterwards — the differential scan that makes an existing banner actually mean something.
- Reject worksPositive finding of the differential scan: after clicking “reject”, no tracking services were contacted and no tracking cookies were set.
- Images without an origin noteFor apps that generate images, checks whether the invisible “made by an AI” note is still attached to what they serve.
- Service not named in the privacy policyCross-checks the services your app actually contacts in the browser against the text of your privacy policy.
- AI interaction without a visible noticeFinds AI endpoints in the client without a visible notice that users are interacting with an AI.
- Tracking cookie with a very long lifetimeMeasures the lifetime of tracking cookies and reports anything beyond one year.
- Consent with pre-ticked boxesFinds already ticked checkboxes inside the consent interface.
- Imprint: mandatory details not foundFetches the imprint page and looks for a postal address, a contact option and the identifier of the jurisdiction (VAT ID/commercial register, Firmenbuch/UID, CHE-UID/commercial register).
- Privacy policy: mandatory details not foundFetches the privacy policy and looks for the controller, data subject rights, the right to complain and the retention period.
- Form without a privacy noticeFinds forms with an email field that have no reference to the privacy policy nearby.
- Consent theatre (composite)Summarises the case where a banner with a reject option exists and consent-dependent services load anyway — before the decision or after clicking “reject”.
Scaling & cost
- Queries without a limitFinds database queries loading entire tables without `limit`/`range` — fine at 10 rows, fatal at 10,000.
- Very large JavaScript bundleMeasures shipped script bytes against a budget — every megabyte costs load time and mobile drop-offs.
- Images without optimisationCounts images without dimensions or modern formats — drives traffic cost and layout shift.
- No caching strategyChecks whether responses carry cache directives — without them every request pays full price.
- Responses uncompressedChecks whether text responses are served with gzip/brotli.
- Queries inside a loop (N+1)Detects `await` inside a loop over records — one round trip per row.
- Heavy first page loadMeasures the bytes actually transferred on first load in the browser.
- Scaling signals unremarkableStandardised classification when bundle, caching and images stay within budget.
- The server responds slowlyMeasures how long the server takes before a page starts loading — the start page and the first subpages the scan loads anyway, excluding responses from a cache.
Accessibility
- No language declarationChecks the `lang` attribute on `<html>` — without it screen readers use the wrong language.
- Images without alternative textCounts `<img>` elements without an `alt` attribute in the shipped markup.
- Form fields without labelsFinds inputs with no associated label or `aria-label` — unusable with a screen reader.
- Accessibility violations in the rendered stateRuns axe-core against the fully rendered page and reports violations by severity — the check static analysis cannot perform.
- Accessibility law likely applicableRecords signals of consumer sales or services — the German BFSG then applies to the digital offering.
- Accessibility signals unremarkableStandardised classification when language, alt texts and labels exist in the markup.
Architecture & SEO
- Platform detectedDetects the no-code/AI platform in use and weights its typical gaps.
- Framework stack detectedDetects the framework stack for framework-native builds (Cursor, Claude Code, classic).
- Domain ownership provenConfirms a completed proof (DNS TXT or file) and unlocks the active checks.
- Throwaway account created for the checkNames the account the scan created via your self-service signup to prove tenant isolation — so it stays deletable.
- Consent provider detectedNames the embedded consent management platform (Usercentrics, OneTrust, Cookiebot, Borlabs, …) — or that only Google's Consent Mode is present without a banner.
- Services and routes of the app detectedReads from the shipped JavaScript what the app talks to: own routes, backend services (Supabase, Firebase …), AI providers and other third-party services — as a list with method and location.
- Server functions detected that may incur costsNames the server functions the app calls whose name suggests AI, SMS or email sending — candidates for cost abuse whose protection only the source code shows.
- Test data could not be removedNames records the write check created and could not delete again.
- Content rendered client-side onlyDetects empty root containers with almost no text in the initial HTML.
- Basic metadata missingChecks page title and meta description in the document.
- Unchanged template titleDetects generator default titles such as “Vite + React” — a sign of unreviewed tool defaults.
- No canonical linkChecks whether the page declares a canonical URL (duplicate-content protection).
- Non-existent paths answer with 200Requests a random path: if the app serves content with status 200 there, neither browsers nor crawlers can detect errors.
- No robots.txt and no sitemapChecks whether the target serves crawl directives and a sitemap.
- Placeholder content in the shipped stateScans the visible text of the reached pages for dummy copy, sample details and unreplaced template fields — and the embedded images for placeholder services such as picsum.photos or placehold.co.
- Mixed forms of address on one pageCounts the informal and formal German forms of address in a page's visible text and reports when two appear side by side.
- Very large document — partial assessmentRecords that the HTML document exceeded the inspection limit and was only assessed up to that point. Not a defect, but a statement about coverage.
- Service worker detectedDetects a registered service worker (or one at `/sw.js` / `/service-worker.js`) and analyses its source like a bundle.
- Progressive web app detectedDetects a web app manifest linked via `<link rel="manifest">` that carries a name or start URL.
- Development address in shipped codeFinds calls to localhost, 127.0.0.1, private network addresses or tunnels (ngrok) in the shipped JavaScript — leftovers from development that go nowhere for users.
- Form without a recipientFinds forms with input fields that have no target address in the HTML, on pages whose fully read code names no recipient either — no address to send to, no backend, no form service.
- Call-to-action leads nowhereChecks the home page buttons whose text is a call to action (“Get started”, “Book a demo”, “Jetzt starten”): does their target point to a page that does not exist? And on pages without JavaScript: do they have a target at all?
- Requests go to a development machineFinds shipped code that sends its requests to localhost, a private network address or a tunnel — these requests never arrive for users.
- A setting was missing when you deployedFinds addresses in the shipped code that contain the word “undefined” or “null” instead of your database or server — the setting behind them was not set when you deployed.
- The app's database is not answeringChecks whether the Supabase project behind the app is paused or no longer exists — then nothing that needs data works.
- The page stays blank when loadingOpens the app in a real browser and checks whether anything is visible after loading. Blank and an error at the same time — that is the “white screen”.
- The app's data requests fail while loadingObserves in the browser whether requests to the app itself or its database end in a server error or network error while loading.
- Server reveals internal processing stepsChecks whether the Server-Timing header names the app's own steps — not just the ones a CDN adds on its own.
Code quality
- Source maps publicDetects shipped source maps that allow reconstructing the original source code.
- Debug leftovers in the shipped buildFinds `console.log`, `debugger` or TODO markers in the production bundle.
- Code internals only partly visibleTransparency finding: without source maps or repo access, code quality cannot be judged seriously.
- Not all bundles analysedRecords that bundle analysis stopped at the budget limit — findings stay valid, the absence of findings says nothing.
- Source code reconstructed from source mapsTransparency finding: states how many original files were read from public source maps.
- Deep-scan coverageTransparency finding: states how many repository files were analysed read-only.
- No files readableReports a deep scan with no source files read — the result carries no meaning.
- Repository read only partiallyReports that file selection hit the budget limit — absence-based rules stay silent and a GO verdict is ruled out.