VibeCheck

What does a VibeCheck report look like?

This page is sample data so the report UI can be reviewed without a live target. It is not the result of a scan in this environment. Real scans require ownership proof and consent.

VibeCheck 96/100
VibeCheck 96/100. Public badges show the score and date only.
rescan needed, VibeCheck 96/100
rescan needed, VibeCheck 96/100. Public badges show the score and date only.

Sample report

0/100 Critical risk
Higher is safer. Each issue lowers the score; critical issues cost the most.

17 findings

Origin: Sample application

5
critical
10
high
2
medium
0
low
0
info

Score starts at 100. Critical −25, high −12, medium −6, low −2.

This score reflects a scan at one point in time. It is not a guarantee of security, and it is not a promise that the app will stay in this condition. Run a new scan after you ship changes.

Secrets & leaked credentials

1 finding

2 checks ran

Cloud backends & storage

3 findings

5 checks ran

Access control

4 findings

8 checks ran

Injection & XSS

2 findings

3 checks ran

Authentication & session

3 findings

5 checks ran

Transport & headers

2 findings

8 checks ran

Information disclosure

1 finding

8 checks ran

Dependencies

1 finding

1 checks ran

Priority issues

criticalCloud backends & storageHeadline 1

Firebase data is world-readable

Evidence. The database URL returned JSON without a user credential. Top-level keys: users.

Impact. The client config points at a database that returns data without a signed-in user. Anyone who knows the project URL can read it.

Location.

  • GET https://sample.example/firebase-rules-open

Proof.

Request: GET https://sample.example/firebase-rules-open

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/firebase-rules-open with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Start from deny-all rules, then open only the paths a signed-in user should see.

{
  "rules": {
    ".read": false,
    ".write": false,
    "users": {
      "$uid": {
        ".read": "auth != null && auth.uid === $uid",
        ".write": "auth != null && auth.uid === $uid"
      }
    }
  }
}

Deploy rules before shipping the client config. Do not leave ".read": true or ".write": true on a parent path.
criticalCloud backends & storageHeadline 1

Supabase table is readable with the public client key

Evidence. The public client key could read users (2 row sample, fields id, email). Write methods are advertised for this table.

Impact. Anyone who loads the app can read rows with the anon key that ships in the browser. Signup data, emails, and private records are exposed even when the UI hides them.

Location.

  • GET https://sample.example/supabase-rls-disabled

Proof.

Request: GET https://sample.example/supabase-rls-disabled

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/supabase-rls-disabled with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Turn on row level security and add a policy before exposing the table to the anon role.

alter table public.users enable row level security;

create policy "read own row"
on public.users
for select
to authenticated
using (auth.uid() = id);

Repeat for every table in the public schema. In the Supabase dashboard, confirm the anon role cannot select rows you would not paste into a public chat.
criticalSecrets & leaked credentialsHeadline 2

Private credential embedded in client code

Evidence. Public JavaScript included a service-role credential and a Stripe secret key. The values were redacted.

Impact. A service-role key, secret API key, or private token is in JavaScript the browser downloads. Anyone can copy it and act as your server.

Location.

  • GET https://sample.example/assets/app.js

    Line 12, column 7

    Secret sk_t…0000

Proof.

Request: GET https://sample.example/assets/app.js

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/assets/app.js with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Remove the secret from client code and rotate it now. The old value is public.

1. Revoke or roll the key in the provider dashboard.
2. Keep the new value in a server-only variable. Do not prefix it with NEXT_PUBLIC_ or VITE_.
3. Call the provider from a server route, and return only the data the page needs.

Search the repo for the leaked prefix and for NEXT_PUBLIC_ / VITE_ variables that hold private keys.
criticalAccess controlHeadline 3

Admin page or API responds without authentication

Evidence. These paths returned privileged content with no session cookie: /admin, /api/admin/users.

Impact. An admin page or API returns privileged data to a request that sent no session. Hiding the link in the UI does not protect it.

Location.

  • GET https://sample.example/admin-surface-open

Proof.

Request: GET https://sample.example/admin-surface-open

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/admin-surface-open with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Check the session on the server before returning admin data.

// example server handler
const session = await getSession(request);
if (!session || session.role !== "admin") {
  return new Response("Forbidden", { status: 403 });
}

Do this in the route that reads or writes data, not only in a client component. Return 401 or 403 when the cookie is missing.
highAccess controlHeadline 4

Sequential ids return other users' records without auth

Evidence. GET /api/users/1 and GET /api/users/2 both returned different records with no session cookie.

Impact. Changing a number in the URL returns a different person's record. Attackers enumerate ids and download the collection.

Location.

  • GET https://sample.example/idor-sequential-ids

Proof.

Request: GET https://sample.example/idor-sequential-ids

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/idor-sequential-ids with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Authorize every object on the server. Do not trust the id in the URL.

const row = await db.order.findFirst({
  where: { id: params.id, userId: session.userId },
});
if (!row) return new Response("Not found", { status: 404 });

Prefer unguessable ids as a backup, but the real fix is the ownership check. A read of someone else's id should look the same as a missing id.
highCloud backends & storageHeadline 5

Storage bucket is publicly listable

Evidence. The storage API listed 1 bucket: avatars.

Impact. The bucket listing is public, so files you thought were unlisted can be enumerated and downloaded.

Location.

  • GET https://sample.example/public-storage-bucket

Proof.

Request: GET https://sample.example/public-storage-bucket

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/public-storage-bucket with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Turn off public listing and serve files through a short-lived signed URL.

- Supabase: set the bucket to private, then create signed URLs on the server after you check the user.
- Firebase Storage: use rules that require auth, for example
  allow read: if request.auth != null && request.auth.uid == userId;
- S3-style buckets: disable public ACLs and block public access at the account level.

A file can stay downloadable by a direct link you handed to its owner without letting the world list every object.
highAccess controlHeadline 6

Premium access is decided only in the browser

Evidence. Client code gates premium access with a localStorage flag named plan.

Impact. The paywall is a local flag. Changing local storage or a client variable unlocks paid features without a payment.

Location.

  • GET https://sample.example/client-side-paywall

Proof.

Request: GET https://sample.example/client-side-paywall

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/client-side-paywall with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Record the purchase on the server and check it there before sending paid data.

const entitled = await db.subscription.findFirst({
  where: { userId: session.userId, status: "active" },
});
if (!entitled) return new Response("Payment required", { status: 402 });

The browser may hide a button, but the API that returns the premium payload has to do the check. Confirm the Stripe webhook updates that row only after a verified event.
highAuthentication & sessionHeadline 6

Stripe webhook accepts requests without a signature

Evidence. An unsigned empty POST was accepted: /api/stripe/webhook returned 200.

Impact. Anyone can POST a fake payment event and mark themselves as paid if the handler trusts the body.

Location.

  • POST https://sample.example/stripe-webhook-unsigned

Proof.

Request: POST https://sample.example/stripe-webhook-unsigned

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send POST https://sample.example/stripe-webhook-unsigned with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Verify the Stripe-Signature header with the webhook secret before reading the event.

const body = await request.text();
const signature = request.headers.get("stripe-signature");
let event;
try {
  event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET);
} catch {
  return new Response("Invalid signature", { status: 400 });
}

Reject missing or invalid signatures with 400. Do not grant access from the success redirect alone.
highAccess controlHeadline 7

AI endpoint accepts unauthenticated requests

Evidence. POST /api/chat returned 200 with no session cookie.

Impact. An open AI route spends your provider budget and can be driven by anyone who can reach the URL.

Location.

  • POST https://sample.example/llm-endpoint-open

Proof.

Request: POST https://sample.example/llm-endpoint-open

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send POST https://sample.example/llm-endpoint-open with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Require a session and a rate limit before calling the model.

const session = await getSession(request);
if (!session) return new Response("Unauthorized", { status: 401 });
await rateLimit(`ai:${session.userId}`, { max: 20, windowSeconds: 3600 });

Keep the provider key on the server. Return a generic error when the limit is hit.
highInjection & XSSHeadline 7

AI endpoint echoes hidden instructions

Evidence. POST /api/chat included hidden instruction text in the response. The text was not copied into this report.

Impact. A short user message got the route to repeat system instructions or other hidden prompt text. Those instructions often include secrets, policies, or private business rules.

Location.

  • POST https://sample.example/llm-prompt-leak

Proof.

Request: POST https://sample.example/llm-prompt-leak

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send POST https://sample.example/llm-prompt-leak with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Do not interpolate raw system instructions into a response, and do not trust the model to keep them.

- Authenticate the route and rate-limit it.
- Keep secrets out of the prompt. Pass them as server-side tool calls instead.
- Add a server-side check that refuses to return the system prompt text.
- Treat user text as data, not as a new instruction channel, and filter outputs before they reach the client.
criticalInformation disclosureHeadline 8

Environment file is publicly downloadable

Evidence. These paths returned environment files: /.env.

Impact. The site serves an env file. Even values that are not cloud keys often include database URLs and session secrets.

Location.

  • GET https://sample.example/exposed-dotenv

Proof.

Request: GET https://sample.example/exposed-dotenv

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/exposed-dotenv with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Remove the file from the public directory and block the path at the host.

# example static host / edge rule
location ~ /\.env { deny all; }

Rebuild without copying .env into the web root. Rotate any secret that was in the file.
highAuthentication & sessionHeadline 9

Client code assigns a privileged role at signup

Evidence. Public client code sends role: "admin" during signup.

Impact. The browser chooses a role such as admin. If the server stores that field, anyone can sign up as an administrator.

Location.

  • GET https://sample.example/signup-privileged-role

Proof.

Request: GET https://sample.example/signup-privileged-role

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/signup-privileged-role with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Ignore any role sent by the client. Set roles on the server from an allow-list you control.

const { email, password } = await request.json();
await db.user.create({
  data: { email, passwordHash, role: "member" },
});

Promote someone to admin in a separate server-only action that checks the caller is already an admin.
mediumAuthentication & sessionHeadline 9

Auth callback redirects to an arbitrary host

Evidence. /auth/callback redirected to example.com based on the next parameter.

Impact. A callback parameter sends the browser to another site. After login, people can be walked onto a lookalike page that steals the session they just created.

Location.

  • GET https://sample.example/open-redirect

Proof.

Request: GET https://sample.example/open-redirect

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/open-redirect with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Allow only relative paths or an explicit list of hosts.

function safeNext(value: string | null) {
  if (!value || !value.startsWith("/") || value.startsWith("//")) return "/dashboard";
  return value;
}
return redirect(safeNext(nextParam));

Do not pass a full URL from the query string into the Location header.
highTransport & headersHeadline 10

CORS allows credentials from any origin

Evidence. A cross-origin request was answered with Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true.

Impact. A browser on another site can send credentialed requests and read the response. That turns a logged-in user's cookies into a cross-site data leak.

Location.

  • GET https://sample.example/cors-credential-wildcard

Proof.

Request: GET https://sample.example/cors-credential-wildcard

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/cors-credential-wildcard with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Reflect only origins you operate, and avoid combining a wildcard with credentials.

const allowed = new Set(["https://app.example.com"]);
const origin = request.headers.get("origin");
if (origin && allowed.has(origin)) {
  headers.set("Access-Control-Allow-Origin", origin);
  headers.set("Vary", "Origin");
}

Do not send Access-Control-Allow-Credentials: true together with Access-Control-Allow-Origin: *.
mediumTransport & headersHeadline 10

Content-Security-Policy header is missing

Evidence. The homepage response did not include a Content-Security-Policy header.

Impact. Without a content security policy, a single injected script can talk to any host and read the page.

Location.

  • GET https://sample.example/missing-csp

Proof.

Request: GET https://sample.example/missing-csp

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/missing-csp with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Send a policy that limits scripts to your own origin. Start strict and loosen only where a host is required.

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Add the hosts your app actually calls, such as your API and image CDN. Avoid unsafe-inline and unsafe-eval on script-src.

Secrets & leaked credentials

Headline findings for this family are in the priority list above.

Cloud backends & storage

Headline findings for this family are in the priority list above.

Access control

Headline findings for this family are in the priority list above.

Injection & XSS

highInjection & XSS

Query text is reflected into HTML without encoding

Evidence. The q query parameter was copied into the HTML response without encoding a < character.

Impact. A crafted link can run script in a visitor's session because the server copies the query string into HTML as markup.

Location.

  • GET https://sample.example/reflected-xss

Proof.

Request: GET https://sample.example/reflected-xss

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/reflected-xss with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Encode untrusted text when you insert it into HTML, or render it as text.

function escapeHtml(value) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

In React, avoid dangerouslySetInnerHTML for request data. Pair this with a Content-Security-Policy that does not allow unsafe inline scripts.

Authentication & session

Headline findings for this family are in the priority list above.

Transport & headers

Headline findings for this family are in the priority list above.

Information disclosure

Headline findings for this family are in the priority list above.

Dependencies

highDependencies

Public manifest pins a dependency with a known vulnerability

Evidence. The public manifest lists lodash@4.17.15 (CVE-2021-23337: Prototype pollution in lodash before 4.17.21).

Impact. The deployed app advertises a library version with a published vulnerability, and the manifest itself is public.

Location.

  • GET https://sample.example/vulnerable-dependency

Proof.

Request: GET https://sample.example/vulnerable-dependency

  • user-agent: VibeCheckScanner/0.1 (+https://vibecheck.textmatchstudio.com/scanner-info)
  • accept: */*

Response.

HTTP 200
content-type: application/json

{"sample":true}

Steps to reproduce.

  1. Send GET https://sample.example/vulnerable-dependency with no session cookie.
  2. The response matches the evidence in this sample finding.

Fix.

Upgrade the package past the affected version and redeploy. Then stop publishing the manifest.

npm install lodash@4.17.21

Use the fixed version named in the finding. Remove package.json and the lockfile from the public host so version details stay in your repo.

What this scan does not cover

  • Business-logic flaws, such as a workflow that does the wrong thing for a valid user.
  • Write or delete abuse. Probes are read-only, so a bug that only appears when data is changed is not proven.
  • Source code and dependencies, unless a repository is connected or a manifest is already public.
  • Load testing and brute-force testing.
  • Mobile apps and backends that are not part of this web origin.

A manual pentest is a better fit when you need those covered in depth.