Promptshelf
Vibe Code Security Checklist — 25 items
Pre-launch audit
Vibe-Code · 25 checks · 5 categories

Twenty-five questions. Before you ship.

Vibe-coded apps ship faster than any prior generation of software. They also ship with vulnerabilities. Multiple 2024–2026 security audits found the majority of AI-generated code contains at least one critical vulnerability when shipped without a security pass. This is a 30-minute self-audit — five severity-ranked categories, five questions each, answered honestly. Share the result. Fix the gaps.

0 / 25
0 / 10 critical 0%
Review needed
Jump to remediation guide ↓
01 / 05

Input & output validation

Most XSS, SQLi, and RCE vulnerabilities trace back to a missing validation or encoding step. Five checks.

Critical Input & Output Validation

0 / 5
  • 00 ·All user inputs sanitized before display — using textContent instead of innerHTML for dynamic content, or an HTML escaper for cases that need markup.

    Remediation

    Default to element.textContent = userInput. Reach for innerHTML only when you genuinely need to inject markup, and only after passing the input through an HTML escaper (or a vetted sanitizer like DOMPurify for rich text).

    // vulnerable: any <script> in input runs
    el.innerHTML = userComment;
    
    // safe: text-only insertion
    el.textContent = userComment;
    
    // safe: rich text, sanitized
    el.innerHTML = DOMPurify.sanitize(userComment);
    Reference: OWASP XSS · CWE-79
  • 01 ·SQL / NoSQL queries use parameterized inputs — never string concatenation or template literals built from user data.

    Remediation

    Pass user values as bound parameters; the database driver handles escaping. AI assistants frequently default to template-literal queries because they read more naturally — review every generated query for this pattern.

    // vulnerable: SQLi via concatenation
    db.query(`SELECT * FROM users WHERE email = '${email}'`);
    
    // safe: parameterized
    db.query('SELECT * FROM users WHERE email = $1', [email]);
    Reference: OWASP SQLi · CWE-89
  • 02 ·File uploads validate type, size, AND magic-byte content — extension alone is forgeable.

    Remediation

    Cap size on the server. Read the first few bytes and verify against the declared MIME type. Reject anything that does not match. Do not trust the Content-Type header from the client — it is supplied by the uploader.

    // detect by magic bytes, not extension
    import { fileTypeFromBuffer } from 'file-type';
    const detected = await fileTypeFromBuffer(buffer);
    if (!ALLOWED_MIMES.has(detected?.mime)) reject();
    if (buffer.length > MAX_BYTES) reject();
    Reference: OWASP Unrestricted Upload · CWE-434
  • 03 ·Server-side validation duplicates client-side validation — the API is reachable directly without going through the form.

    Remediation

    Treat client-side validation as a UX nicety only. Every constraint enforced in the form must be re-asserted on the server. Use the same validation schema (Zod, Yup, Joi) on both sides if possible to keep them in sync.

    // shared schema, enforced on the server
    const SignupSchema = z.object({
      email: z.string().email(),
      age: z.number().int().min(13).max(120)
    });
    
    app.post('/signup', (req, res) => {
      const parsed = SignupSchema.safeParse(req.body);
      if (!parsed.success) return res.status(400).json(parsed.error);
      // ... use parsed.data
    });
    Reference: OWASP Improper Validation · CWE-20
  • 04 ·Output encoding matches the destination context — HTML escaping for HTML, JS escaping for inline JS, URL encoding for query params, shell escaping for shell calls.

    Remediation

    One escaper does not fit all contexts. HTML-escaping a string that lands inside a JS string literal still allows breakouts via backslash. Shell-execute calls need execFile with array args, never exec with a built string.

    // command injection
    exec(`grep ${pattern} file.txt`);
    
    // safe
    execFile('grep', [pattern, 'file.txt']);
    Reference: OWASP Injection Theory · CWE-78
02 / 05

Authentication & authorization

Most account-takeover and data-leak incidents trace back to this layer. Five checks.

Critical Authentication & Authorization

0 / 5
  • 05 ·Auth tokens stored in httpOnly + Secure + SameSite cookies — NOT in localStorage or sessionStorage where any XSS can steal them.

    Remediation

    Vibe-coded auth flows often use localStorage because the AI suggests it as the simplest option. The drawback: an XSS anywhere on the origin can read it. httpOnly cookies are JS-invisible, so the same XSS cannot exfiltrate the session.

    // set on login response
    res.cookie('session', token, {
      httpOnly: true,
      secure: true,
      sameSite: 'lax',
      maxAge: 1000 * 60 * 60 * 24 * 7
    });
    Reference: OWASP httpOnly · CWE-1004
  • 06 ·Password reset tokens are time-limited (15-60 min), single-use, and cryptographically random — not predictable IDs or sequential counters.

    Remediation

    Generate the token from a CSPRNG. Store its hash, not the raw value. Set an expiry. Mark consumed after use. AI assistants sometimes propose reset flows that email a base64-encoded user ID — a guessable identifier is not a token.

    import crypto from 'node:crypto';
    const token = crypto.randomBytes(32).toString('hex');
    await db.passwordResets.create({
      userId, tokenHash: sha256(token),
      expiresAt: Date.now() + 30 * 60 * 1000,
      consumedAt: null
    });
    sendEmail(user.email, `https://app/reset?t=${token}`);
    Reference: OWASP Forgot Password · CWE-640
  • 07 ·IDOR check: every object-level access is verified on the backend — GET /orders/123 confirms order 123 belongs to the requesting user, not just that they are authenticated.

    Remediation

    Authentication answers "who are you"; authorization answers "what can you access". Vibe-coded APIs often check the first and skip the second. Add an explicit ownership predicate to every fetch.

    // IDOR: any logged-in user can read any order
    app.get('/orders/:id', auth, async (req, res) => {
      res.json(await db.orders.findById(req.params.id));
    });
    
    // safe: scope to the owner
    app.get('/orders/:id', auth, async (req, res) => {
      const order = await db.orders.findOne({
        id: req.params.id, userId: req.user.id
      });
      if (!order) return res.status(404).end();
      res.json(order);
    });
    Reference: OWASP A01 Broken Access Control · CWE-639
  • 08 ·Admin / privileged routes have backend role checks — frontend hiding of an admin link is not a security boundary.

    Remediation

    Hiding the admin button only stops accidental clicks. Anyone who opens DevTools or curls the API directly bypasses it. Wrap admin routes in a server-side role guard middleware.

    function requireAdmin(req, res, next) {
      if (req.user?.role !== 'admin') {
        return res.status(403).json({ error: 'forbidden' });
      }
      next();
    }
    app.delete('/admin/users/:id', auth, requireAdmin, ...);
    Reference: OWASP A01 · CWE-285
  • 09 ·Auth endpoints have rate limiting — login, registration, password reset, and OTP verify each cap brute force attempts per IP and per account.

    Remediation

    Rate-limit by both IP and account identifier. Account-only is bypassable via IP rotation; IP-only fails when many users share a NAT. Lock to a small number of failures per minute, with exponential backoff or temporary lockout on repeated failures.

    // per-IP: 10 / minute
    // per-account: 5 / 15 minutes
    const ipLimiter = rateLimit({ windowMs: 60_000, max: 10 });
    const acctLimiter = rateLimit({
      windowMs: 15 * 60_000, max: 5,
      keyGenerator: req => req.body?.email
    });
    app.post('/login', ipLimiter, acctLimiter, ...);
    Reference: OWASP Auth · CWE-307
03 / 05

Data exposure & storage

Five checks covering API field whitelisting, log sanitization, secret hardcoding, error leakage, and uploaded-file storage.

High Data Exposure & Storage

0 / 5
  • 10 ·API responses include only the fields the requesting user is authorized to see — no SELECT * shoveled to JSON, no internal flags leaking.

    Remediation

    Define a serializer per endpoint that whitelists fields explicitly. Vibe-coded APIs commonly return the full row including password_hash, internal_notes, or is_admin — use a "deny by default" projection.

    // leaks password_hash, is_admin, ...
    res.json(await db.users.findById(id));
    
    // safe: explicit shape
    function publicUser(u) {
      return { id: u.id, name: u.name, avatar: u.avatar };
    }
    res.json(publicUser(await db.users.findById(id)));
    Reference: OWASP API3 · CWE-213
  • 11 ·Sensitive data — passwords, auth tokens, full PII, payment cards — is never written to logs.

    Remediation

    Default request loggers capture full payloads — including the password field from POST /login. Configure a redactor that strips known-sensitive paths before they hit your log sink.

    // pino redact example
    const logger = pino({
      redact: [
        'req.body.password',
        'req.body.token',
        'req.headers.authorization',
        'req.headers.cookie',
        '*.creditCard'
      ]
    });
    Reference: CWE-532 · OWASP A09 Logging Failures
  • 12 ·Secrets are not hardcoded in source AND not committed in .env to version control — even briefly, even in a private repo.

    Remediation

    Add .env to .gitignore. Run gitleaks in pre-commit. If a secret has ever touched a public commit, rotate it immediately — assume scrapers have already grabbed it.

    # .gitignore
    .env
    .env.local
    .env.*.local
    
    # add gitleaks as a git pre-commit hook
    brew install gitleaks
    echo 'gitleaks protect --staged -v' > .git/hooks/pre-commit
    chmod +x .git/hooks/pre-commit
    Reference: gitleaks · CWE-798
  • 13 ·Production error messages are generic — stack traces, file paths, ORM details, and SQL fragments never reach the user.

    Remediation

    Catch all unhandled errors at the top level. Log the full stack server-side; return a generic message to the client. Ship a development-vs-production switch and verify production really hides the details.

    app.use((err, req, res, next) => {
      logger.error(err);
      if (process.env.NODE_ENV === 'production') {
        return res.status(500).json({ error: 'internal_error' });
      }
      res.status(500).json({ error: err.message, stack: err.stack });
    });
    Reference: OWASP Improper Error Handling · CWE-209
  • 14 ·User-uploaded files are stored outside the web root, OR served only via signed time-limited URLs from object storage.

    Remediation

    If uploads land in /public/uploads/ they are world-readable forever. Stash them in S3 / R2 / GCS, then mint short-lived signed URLs only after verifying the requesting user owns the file. Signing without an ownership check is IDOR (CWE-639).

    // AWS SDK v3 (v2 is end-of-support since 2025-09-08).
    // Authorize FIRST, then sign. Ownership check is non-optional.
    import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
    import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
    const s3 = new S3Client({});
    // Scope the lookup by ownerId so a guessed/forged key cannot leak.
    const file = await db.files.findOne({
      key: req.params.key,
      ownerId: req.user.id,
    });
    if (!file) return res.status(404).json({ error: "not_found" });
    const url = await getSignedUrl(
      s3,
      new GetObjectCommand({ Bucket, Key: file.key }),
      { expiresIn: 300 }
    );
    res.json({ url });
    // SDK v2: use s3.getSignedUrlPromise() (sync getSignedUrl has no await)
04 / 05

Dependencies & environment

Supply-chain attacks are rising sharply since AI-assisted development became mainstream. Five checks.

High Dependencies & Environment

0 / 5
  • 15 ·Every package in package.json / requirements.txt exists in the registry, has a non-zero install count, and is not a typo of a popular package (slopsquatting defense).

    Remediation

    AI assistants hallucinate package names — about 21.7% of OSS-model recommendations and 5.2% of commercial recommendations point at packages that do not exist. Attackers pre-register the most-hallucinated names. Eyeball every direct dep and verify weekly downloads on npm / PyPI before installing.

    # quick sanity check
    npm view express versions --json | tail -5
    npm view express dependencies
    # weekly downloads
    curl -s https://api.npmjs.org/downloads/point/last-week/express \
      | jq .downloads
    Reference: Aikido on slopsquatting · CWE-1357
  • 16 ·npm audit / pip audit / cargo audit shows zero CRITICAL vulnerabilities — patched, justified, or version-pinned with documented exception.

    Remediation

    Run the audit in CI. Fail the build on CRITICAL. For findings you decide not to patch, add a documented allow-list entry with a reason and an expiry date — never silent ignore.

    # GitHub Action snippet
    - run: npm audit --audit-level=critical
    # or with allow-list
    - run: npx audit-ci --critical \
        --allowlist CVE-2024-12345
    Reference: npm audit docs · OWASP A06
  • 17 ·Dev dependencies are excluded from the production build — no test runners, mock data generators, or debugging utilities ship to production.

    Remediation

    Install with --production in the deploy step. Confirm bundles do not include dev-only utilities by inspecting the output. Vibe-coded apps frequently bundle the entire dev-dep tree because the AI scaffolded a single dependencies block instead of splitting into devDependencies.

    npm ci --omit=dev
    # or in Dockerfile
    RUN npm ci --only=production
    # verify bundle
    npx webpack-bundle-analyzer dist/stats.json
  • 18 ·HTTPS is enforced everywhere — HSTS header set, no mixed-content warnings, no http:// fetches in browser code.

    Remediation

    Redirect HTTP → HTTPS at the edge. Set HSTS for at least one year. Audit fetch calls and image / script tags for any plain-http URL.

    // Express HSTS via helmet
    app.use(helmet.hsts({
      maxAge: 31536000, // 1 year
      includeSubDomains: true,
      preload: true
    }));
    Reference: OWASP Secure Headers · CWE-319
  • 19 ·CORS is configured explicitly — Access-Control-Allow-Origin lists known origins, never * in production for credentialed endpoints.

    Remediation

    Wildcard CORS plus credentials is broken (browsers reject it), but wildcard CORS without credentials still leaks public endpoint data to any site. Allow-list specific origins for the production API.

    app.use(cors({
      origin: [
        'https://app.example.com',
        'https://staging.example.com'
      ],
      credentials: true
    }));
    Reference: MDN CORS · CWE-942
05 / 05

Configuration & deployment

Configuration drift is the slow-moving risk most teams underestimate. Five checks.

Medium Configuration & Deployment

0 / 5
  • 20 ·Security headers set: Content-Security-Policy, X-Frame-Options (or CSP frame-ancestors), Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy.

    Remediation

    Use Helmet (Express) or your framework's equivalent. Test the result on securityheaders.com before launch.

    app.use(helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          scriptSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'"] // prefer nonces/hashes over unsafe-inline
        }
      }
    }));
  • 21 ·Debug mode and verbose error logging are disabled in production — NODE_ENV=production, DEBUG=false, framework debug toolbars off.

    Remediation

    Verify on the deployed environment, not just in code. Many vibe-coded apps default to development mode because the AI never wrote a production switch — check /health or a debug-only route to confirm.

    # inspect what production actually has set
    heroku config -a my-app  # or
    flyctl secrets list -a my-app
    # Django: confirm DEBUG=False
    python -c "import django.conf; print(django.conf.settings.DEBUG)"
    Reference: CWE-489
  • 22 ·Default database / admin credentials changed before first public deployment — no admin/admin, postgres/postgres, or framework default left in place.

    Remediation

    Generate strong random passwords. Rotate any default the AI scaffolded into .env.example. Audit your Postgres / Redis / Mongo for permissive default users.

    # generate
    openssl rand -base64 32
    # rotate Postgres
    ALTER USER postgres WITH PASSWORD '...new...';
    Reference: CWE-798
  • 23 ·API keys rotated if ever committed to git, even briefly, even in a private repo — and rotation script / runbook exists for future incidents.

    Remediation

    If a secret entered git history, treat it as compromised — repo visibility changes, contributors come and go. Rotate at the source provider, then update the deployed environment.

    # scan full git history for leaked secrets
    gitleaks detect
    # or scan all branches
    gitleaks detect --log-opts="--all"
    # rotate AWS access key
    aws iam create-access-key --user-name svc-app
    aws iam delete-access-key --access-key-id AKIA...
    Reference: gitleaks · CWE-540
  • 24 ·AI prompt history reviewed: no "skip auth for now", "ignore the security check", or "just use eval" instructions left in CLAUDE.md / .cursorrules / system prompts before launch.

    Remediation

    The AI-specific failure mode: temporary instructions added during prototyping ("auth is hard, mock it for now") get persisted into the agent config and silently shape future code generations. Grep your AI configuration files for known anti-patterns before going live.

    # grep AI config files for risky instructions
    grep -r -E "(skip|ignore|disable|bypass).{0,30}(auth|security|validation|check)" \
      CLAUDE.md AGENTS.md .cursorrules \
      .github/copilot-instructions.md 2>/dev/null
    # also check for eval / unsafe execution patterns
    grep -r -E "(use eval|use exec|raw shell|no sandbox)" \
      CLAUDE.md .cursorrules 2>/dev/null
    Reference: OWASP LLM Top 10
+  

Remediation guide

Optional inline fix snippets for every item. Free checklist works without it; the unlock is the in-depth fix manual.

Unlock the remediation guide — 5,000 sats

≈ $5 · one-time

Every checklist item gains an inline "Show fix" panel with code-level remediation, vulnerable-vs-fixed snippets, and primary-source links (OWASP, CWE, vendor docs). One 5,000-sat confirmed payment unlocks all seven current priced features site-wide via the same TXID and shared client-side verifier. Verification happens in your browser against mempool.space's public REST API with blockstream.info as fallback — we never see your wallet, no account, no signup, no recurring charge.

Step 01

Send 5,000 sats

Send at least 5,000 sats (≈ $5) to the address below from any BTC wallet. Wait for one on-chain confirmation (~10–30 min) before moving to Step 03 — unconfirmed transactions are rejected to prevent double-spend.

Step 02

Copy the TXID

Your wallet shows the transaction ID after sending. It is 64 hexadecimal characters. Copy it.

Step 03

Paste & verify

Paste the TXID below. Your browser checks the public Bitcoin explorer; once it confirms the transaction is on-chain and pays the unlock address, the remediation panels open immediately and the same TXID unlocks all priced features site-wide.

BTC bc1qs04leape97ner4wqa98n94l9n0gv9aa84eg4ux

No wallet integration, no Lightning, no bridge — plain on-chain BTC. Verification source: mempool.space REST API; blockstream.info fallback. Persistence: localStorage stores the local open/closed state — clearing site data re-locks the panel, but the same confirmed TXID can be re-pasted on any priced page. Need a custom audit instead?

How

How this works

Three things to know before you start.

01 · Honest, not hopeful

Skip what does not apply

Some checks are context-dependent — HTTPS enforcement on a localhost-only LAN tool, for example. The score is informational. The point is to make you think about each category, not grade you.

02 · Sharable scorecard

Paste the URL into a PR

Every checked item is one bit; 25 bits pack into 7 hex chars as ?chk=<7hex>. Pasting a URL pre-checks the items the original auditor cleared. Invalid URLs reject; they do not silently mutate to blank.

03 · Not a substitute for an audit

Hire a real auditor for money apps

This is a self-audit prompt — a way to systematically walk a vibe-coded app through common vulnerability classes before it ships. For consumer-facing or money-handling apps, hire a qualified penetration tester before launch.

FAQ

Frequently asked

A vibe-coded app is software written largely or entirely by prompting an AI coding tool (Cursor, Claude Code, Copilot, Lovable, v0, etc.) and accepting most of its suggestions without line-by-line review. The "vibe" is the developer's intuition that the result looks right — without the deep code-review pass that normally catches security mistakes. Multiple 2024–2026 security audits found the majority of AI-generated code contains at least one critical vulnerability when shipped without a security pass.
OWASP Top 10 covers most categories already; this checklist adds the AI-specific concerns (package hallucination, residual "skip auth" prompts in code) without inflating the list to a 100-item form most developers will close. Twenty-five maps cleanly to five severity-ranked categories of five items each — short enough to actually finish in one sitting, long enough to catch the common classes.
Every checked item is one bit; the 25 bits pack into 7 hex characters as ?chk=<7hex>. URLs round-trip exactly; pasting a teammate's URL pre-checks the items they cleared. Invalid input (wrong length, non-hex, or non-zero padding bits) is rejected — corrupt URLs do not silently mutate to blank state.
A 5,000-sat one-time payment unlocks an inline remediation guide and all seven current priced features on Promptshelf site-wide. Every checklist item gains an expanded "Show fix" panel with code-level instructions, vulnerable-vs-fixed snippets, and links to OWASP / CWE primary sources. The free checklist is fully usable without the unlock; the shared client-side verifier accepts the same TXID across priced pages. Verified client-side via mempool.space; no account.
Some items have context-dependent answers (e.g., HTTPS enforcement on a localhost-only LAN tool). Skip what does not apply; the score is informational, not a certificate. The point is to make you THINK about each category, not to grade you. The scorecard URL is shareable so a teammate can audit your reasoning.
No. This is a self-audit prompt — a way to systematically walk a vibe-coded app through the most common vulnerability classes before it ships. A paid penetration test by a qualified firm catches things a checklist cannot. For consumer-facing or money-handling apps, hire a real auditor before launch.

Found this useful? Tips in BTC always welcome — same address as the remediation unlock. No account, no signup, just paste.

BTC bc1qs04leape97ner4wqa98n94l9n0gv9aa84eg4ux