⏳ This skill is pending AI review.

Scores will appear once the review pipeline completes.

version unknown

paranoid

@kulchankas⭐ 48 stars

>-

Choose how to use this skill

You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.

1. Native installer

This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.

Do not guess an installer command or replace an existing version without reviewing the diff.

2. Complete package recommended

Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.

No complete ProSkills package is published for this listing yet.

3. Prompt-only

Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.

Need only the instruction file?

Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.

No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.

—/10

// RATINGS

⭐GitHub Stars
⭐⭐ 48 on GitHubGitHub ↗

Growing

🟢ProSkills Score
—
📍

Not yet listed on ClawHub or SkillsMP

// README

🕵️ paranoid

An agent skill that pentests the app you're building. /hack-me attacks your own running app the way an attacker would, then closes what it finds:

find  →  prove  →  patch  →  re-verify

Nothing is called a bug until a real HTTP request proves it, no fix is done until that exact exploit stops working, and no class is closed until the same pattern is swept from the paths no probe touched.

Quickstart

Claude Code — one install, gets you the skill and the pentest loop:

/plugin marketplace add kulchankas/paranoid
/plugin install paranoid@paranoid

Then start your app and point it at the loop — plugin skills are namespaced, so it's /paranoid:hack-me here:

python3 my_app.py     # your app, running locally
/paranoid:hack-me     # → http://localhost:<port>

Codex, Cursor, or any agent that reads skills:

npx skills add kulchankas/paranoid/skills/paranoid

Copy commands/hack-me.md into your agent's commands directory (e.g. .claude/commands/) and it's plain /hack-me.

No dependencies, no network calls, no telemetry — it's Markdown your agent reads.


Receipts

On an app we didn't write

The hard claim is code we don't control. Pointed at OWASP VAmPI — a well-known third-party vulnerable API — with nothing but its URL, /hack-me found, proved, patched and re-verified six real bugs:

#FindingOWASP APIStatus
1Unauth /users/v1/_debug dumps every passwordAPI3/5401/403
2Read any user's private book secret (BOLA)API1404
3Register with admin:true → privilege escalationAPI6admin=false
4Change any user's password (account takeover)API1victim untouched
5SQLi in user lookup (UNION-dumps passwords)API8404
6Debugger + stack traces exposedAPI7clean errors

Notably, VAmPI's own global "secure mode" flag closed only four of the six — the critical password dump stayed open until patched. /hack-me caught it by replaying every exploit instead of trusting the flag. Full receipts: examples/vampi/HACKME_REPORT.md.

On a demo app, start to finish

Against a small invoicing API (examples/ledgerlite), an agent told nothing about the app's bugs found four by probing:

#Found by probing the APIClassAfter patch
1Any user reads any invoiceIDOR / broken object auth404
2/admin/users open to anyone logged inbroken function auth403
3/search?email= SQL injection (dumped passwords)SQLi[]
4/profile accepts is_adminmass assignment → privesc400

Every legitimate request still returns 200 afterwards. Reproduce it yourself: python3 examples/ledgerlite/app.py, then run /hack-me. Walkthrough with exact requests and diffs: examples/ledgerlite/HACKME_REPORT.md.

On a shop with no exploit menu

DVWA lists its bug categories in the navigation, so that run tests the fix loop, not discovery. VulnShop renders as a store (NexCart): products, search, account. Pointed at that UI, /hack-me proved five bugs and re-verified each — including a product-id UNION that printed every seed password, and a login of admin' OR '1'='1 that came back as an admin session:

#FindingStatus
1/products?id= dumps every passwordempty listing
2Login bypass to an admin sessionno session
3Search reflects raw HTMLescaped
4Any logged-in user opens /admin403
5Profile save with no CSRF token400

The upstream README names those classes, so this is not a fully blind-to-the-repo find. Receipts: examples/vulnshop/HACKME_REPORT.md.

What /hack-me actually does

  1. Maps your running app and picks the risk classes it's exposed to.
  2. Probes each — one crafted request that only succeeds if the bug is real.
  3. Proves every finding with the actual request/response (no theorizing).
  4. Patches the root cause with a minimal, behavior-preserving fix.
  5. Re-verifies by replaying the exact exploit — a finding isn't closed until it fails.
  6. Sweeps for the same bug on paths no probe touched — a sibling branch, the same resource under a different method, a /v1 copy, a cron job that reaches the same sink. A green re-verify proves the request is dead, not the class.

Step 6 exists because the loop got caught by exactly that: in our own DVWA run an SQL injection was proven, patched and re-verified green while an identical injection sat in the same file, in the branch for the other database backend. Review caught it; the loop hadn't. Anything the sweep fixes but can't reach with a request is reported as "same pattern, fixed, not separately proven" — never as verified.

It knows where routes and auth live in twelve stacks (Next.js, FastAPI, Express, Django, Rails, Flask, Spring Boot, Laravel, Phoenix, Go, NestJS, ASP.NET Core) — see references/frameworks.md. Guardrails apply throughout; see Scope & ethics.

Why this isn't another "write secure code" skill

It started as the obvious thing — guidance telling the agent to write secure code — and then got benchmarked honestly before anyone believed it. The harness (benchmark/) generates the same tasks with and without the skill and runs real exploits against whatever the model writes.

The result was a clean negative:

ModelTasksExploit rate without skillwith skillEffect
Opusall 22 classes, blinded0%0%none
Opus3 isolated functions0%0%none
Fable 5.13 isolated functions0%0%none

On an isolated function a capable model already writes the secure version unprompted — ownership in the WHERE clause, parameterized queries, field allow-lists. Advice adds nothing there. (The harness isn't rigged: it flags deliberately-insecure reference code at 100% and secure code at 0%, and CI asserts that on every push.)

Blinded matters here: the task ids name their own vulnerability, so asking a model for sqli_login.py is itself a security hint. The 22-class run was redone with the specs renamed task_01…task_22, generated outside the repo, with no mention of a benchmark — and the result held. The skill did, however, cost a functional test the baseline passed. [Full

// HOW IT'S BUILT

// FILES

9 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.

  • skills/paranoid/SKILL.md5.9 KB00a7df24ab03upstream ↗
    ---
    name: paranoid
    description: >-
      Write and review code as if it gets pentested tomorrow. Load this whenever
      code touches a trust boundary: authentication, authorization, user input,
      database access, secrets or environment variables, file uploads, outbound
      requests (fetch/webhooks), API or webhook endpoints, or anything that runs
      raw SQL/shell/HTML. Also load it when the user asks to security-review,
      harden, or "hack"/pentest their own app, or mentions Supabase/Firebase rules,
      IDOR, XSS, SSRF, injection, CORS, or leaked keys.
    ---
    
    # paranoid
    
    You are reviewing every line you write as if a bored attacker will read it
    tomorrow. Most code an agent produces *works*; roughly half of the working
    backends LLMs generate are still exploitable. Your job is to be the half that
    isn't.
    
    Three rules, in order:
    
    1. **Name the trust boundary before you write the feature.**
    2. **Apply the secure default for each risk class you cross.**
    3. **Try to break it before you call it done.**
    
    Keep this lightweight. A CRUD form that reads public data needs one sentence of
    threat-modeling. A file upload that shells out to `ffmpeg` needs real care. Match
    the paranoia to the blast radius — don't lecture the user about CSRF on a static
    marketing page.
    
    ---
    
    ## 1. Name the trust boundary (one line, out loud)
    
    Before writing anything that crosses a boundary, state — in the chat, briefly —
    who can reach this code and what they control:
    
    > *"This endpoint takes a `postId` from the client. Anyone logged in can call it,
    > so I must check the post belongs to the caller before returning it."*
    
    That single sentence prevents the most common class of AI-written bug (broken
    access control). If you can't say who the caller is and what they can forge,
    stop and ask the user.
    
    ## 2. Secure defaults per risk class
    
    When you touch one of these, apply the rule. Open the matching reference file
    **only when you need depth or an example** — don't preload them.
    
    | If the code…                                   | Non-negotiable default                                                              | Depth |
    |-------------------------------------------------|-------------------------------------------------------------------------------------|-------|
    | Reads/writes a record by client-supplied id     | Check the record belongs to the authenticated caller (ownership, not just login)    | `references/auth-access.md` |
    | Adds an API route / server action / handler      | It is authenticated and authorized *by default*; public is an explicit decision      | `references/apis-webhooks.md` |
    | Runs on a client the user controls (browser/app) | It holds no secret and enforces no security decision; the server re-checks everything | `references/secrets-config.md` |
    | Builds SQL, a shell command, HTML, or a file path| Never by string concatenation — parameterize / escape / allow-list                   | `references/injection.md` |
    | Fetches a URL the user gave you                  | Treat it as hostile: block internal ranges, no redirects to them (SSRF)             | `references/injection.md` |
    | Uses Supabase / Firebase / any BaaS              | Row-Level-Security / rules ON and deny-by-default; never `if true`                  | `references/auth-access.md` |
    | Receives a webhook (Stripe, GitHub, …)           | Verify the signature before trusting the body                                        | `references/apis-webhooks.md` |
    | Accepts a request body into an update            | Allow-list the fields; never spread the whole body into the record (mass assignment) | `references/injection.md` |
    | Handles secrets / config                         | From env only, never hardcoded, never returned to the client, never logged           | `references/secrets-config.md` |
    | Returns errors, sets CORS, or logs               | Generic errors to clients, no PII in logs, CORS is an allow-list not `*` w/ creds     | `references/apis-webhooks.md` |
    
    The ten failure modes above are the ones LLM-generated code actually ships. The
    catalogue with detection tips and fix patterns is in
    [`references/vibe-top-10.md`](references/vibe-top-10.md).
    
    ### Hard "never introduce" list
    
    Refuse to write these even if asked casually — say why, offer the safe version:
    
    - Auth/permission check that lives **only** in the frontend.
    - `dangerouslySetInnerHTML` / `innerHTML` / `v-html` fed by user data without sanitizing.
    - SQL built with template strings or `+` around user input.
    - `eval`, `exec`, `child_process` with a string that contains user input.
    - Secret, private key, or admin token in code shipped to the browser.
    - A database rule of `allow read, write: if true` (or RLS disabled) on real data.
    - Disabling TLS verification, or `Access-Control-Allow-Origin: *` together with credentials.
    
    ## 3. Try to break it before "done"
    
    For anything that crossed a boundary, do a 30-second adversarial pass and say
    what you checked:
    
    - What happens if I pass **someone else's id**? A **negative / huge / non-numeric** id?
    - If I call this endpoint **logged out**? As a **different user**?
    - If the input is 10 MB? Contains `../`, `<script>`, `'; DROP`, a `file://` URL?
    - If two requests race?
    
    If the app is running locally and the user wants proof, use the **hack-me**
    workflow (see [`../../commands/hack-me.md`](../../commands/hack-me.md)): it
    exploits the user's *own* app on localhost, shows the bug is real, patches it,
    and re-runs to confirm the fix. For where routes and auth live per stack
    (Next.js, FastAPI, Express), see [`references/frameworks.md`](references/frameworks.md).
    
    Before committing a feature, run the gate in
    [`checklists/pre-commit.md`](checklists/pre-commit.md). It's seven questions; if
    they all pass, ship.
    
    ---
    
    ## Scope & safety
    
    This skill is for securing the **user's own** code and pentesting the **user's own**
    running app with their say-so. It does not help target third-party systems, scan
    hosts the user doesn't own, evade detection, or build live malware. If a request
    drifts that way, stop and say so.
    
  • skills/paranoid/references/apis-webhooks.md4.2 KB099ed0dc3319upstream ↗
    # APIs, webhooks & responses
    
    The edge of your app: routes, the webhooks you receive, and what you send back.
    
    ## Deny by default
    
    Every route is authenticated and authorized unless you consciously make it
    public. Structure this so "public" is the annotated exception, not the silent
    default you forgot to change.
    
    ```ts
    // ✓ one wrapper, applied everywhere; public routes are explicit
    const requireAuth = (h) => async (req, res) => {
      const s = await getSession(req);
      if (!s) return res.sendStatus(401);
      req.user = s.user;
      return h(req, res);
    };
    
    app.get('/api/health', publicRoute(handler));      // explicitly public
    app.get('/api/orders', requireAuth(listOrders));    // default
    ```
    
    - Don't rely on a route being "internal" or unguessable. If it's reachable, it's
      reached. Admin endpoints especially.
    - Middleware order matters: auth must run **before** the handler, and a catch-all
      shouldn't accidentally bypass it. In Next.js, remember route handlers and
      server actions are each independently reachable — protect the data layer, not
      just the page.
    - Return `401` for "not logged in", `403` for "logged in but not allowed",
      `404` to hide existence on ownership misses.
    
    ## Input validation at the edge
    
    Parse every body/query/param through a schema before use (see mass assignment in
    `injection.md`). Enforce bounds: max length on strings, ranges on numbers,
    max size on the request body, max items on arrays. Unbounded input is how you get
    both injection and denial-of-service.
    
    ## Rate limiting sensitive routes
    
    Login, signup, password reset, OTP, and anything expensive needs a throttle —
    otherwise: credential stuffing, user enumeration, and cost/DoS.
    
    ```ts
    import rateLimit from 'express-rate-limit';
    app.use('/api/auth', rateLimit({ windowMs: 60_000, max: 10 }));
    ```
    
    - Key by IP **and** account where you can; lock/back off after repeated failures.
    - Make auth responses uniform and constant-ish time so they don't reveal whether
      an account exists ("invalid email or password", never "no such user").
    
    ## Webhooks: verify before you trust
    
    An unverified webhook is an unauthenticated POST that mutates your data. Anyone
    can send `{"type":"payment_succeeded"}`. Verify the provider's signature against
    the **raw** body (not the parsed/re-serialized JSON — re-serializing breaks the
    signature):
    
    ```ts
    // Stripe — needs the raw body
    app.post('/webhooks/stripe',
      express.raw({ type: 'application/json' }),
      (req, res) => {
        let event;
        try {
          event = stripe.webhooks.constructEvent(
            req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
        } catch {
          return res.sendStatus(400); // bad/forged signature
        }
        // event is trustworthy from here
      });
    ```
    
    - GitHub: HMAC-SHA256 over the raw body with your secret, compared to
      `X-Hub-Signature-256` using a **constant-time** compare (`crypto.timingSafeEqual`).
    - The usual skip: `JSON.parse(req.body)` and credit the customer with no HMAC
      check. A forged `payment.succeeded` then lands as an unauthenticated POST.
      Verify first (constant-time, against the raw bytes), parse after.
    - Guard against replay: honor timestamps/tolerances and treat delivery ids as
      idempotency keys so a re-sent event isn't processed twice.
    
    ## Responses, errors, logs, CORS, headers
    
    - **Errors:** generic message + a correlation id to the client; the stack trace
      and SQL error go to your server logs only. Never `res.send(err)` /
      `err.stack` / raw DB errors to the client.
    - **Logs:** no passwords, tokens, full card numbers, or PII. Redact before
      logging; assume logs are less protected than your database.
    - **CORS:** an allow-list of known origins. Never reflect an arbitrary `Origin`
      with `Access-Control-Allow-Credentials: true` — that's an open door with the
      user's cookies. `*` is only acceptable for truly public, credential-less APIs.
    - **Headers:** set the standard set (via `helmet` or equivalent):
      `Content-Security-Policy`, `Strict-Transport-Security`,
      `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY` (or CSP
      `frame-ancestors`), `Referrer-Policy`. HTTPS everywhere; cookies `Secure`.
    - **Data shape:** return only the fields the client needs. Serializing a whole
      DB row leaks `passwordHash`, internal flags, other users' data — select
      explicitly or use a response DTO.
    
  • skills/paranoid/references/auth-access.md4.5 KB99ddaa870160upstream ↗
    # Auth & access control
    
    Broken access control is #1 for a reason: the code looks correct, the tests pass,
    the happy path works — and any user can read any other user's data. Authentication
    ("who are you") is not authorization ("may you touch *this*"). You need both.
    
    ## Ownership on every object lookup
    
    Every read/write keyed on a client-supplied id must be scoped to the caller.
    
    ```ts
    // ✗ authenticated but not authorized
    const doc = await db.document.findUnique({ where: { id } });
    
    // ✓ ownership is part of the query
    const doc = await db.document.findFirst({ where: { id, ownerId: req.user.id } });
    if (!doc) return res.sendStatus(404); // 404, not 403 — don't confirm it exists
    ```
    
    For shared resources, check membership rather than a single owner:
    
    ```ts
    const member = await db.membership.findFirst({
      where: { projectId, userId: req.user.id },
    });
    if (!member) return res.sendStatus(404);
    if (action === 'delete' && member.role !== 'owner') return res.sendStatus(403);
    ```
    
    Rules of thumb:
    - Put the caller's id **in the `where` clause**, not in an `if` after the fetch
      you might forget.
    - Prefer `404` over `403` on ownership misses so you don't leak existence.
    - Do the check server-side, every time — never rely on the client hiding a button.
    - IDs that are enumerable (`1, 2, 3…`) make IDOR trivial; UUIDs help but are
      **not** an access control — still check ownership.
    - Multi-route APIs: don't scope `GET /resource/:id` and forget mutations.
      `PATCH`, `PUT`, and `DELETE` must include the owner check in the mutation
      query (`where: { id, ownerId }`), not just rely on router-level authentication.
    
    ## Function-level authorization
    
    Don't gate only by "is logged in." Gate by role/permission for the specific
    action, on the server:
    
    ```ts
    export const requireRole = (role) => (req, res, next) =>
      req.user?.roles?.includes(role) ? next() : res.sendStatus(403);
    
    app.delete('/api/users/:id', auth, requireRole('admin'), handler);
    ```
    
    Never trust a role sent by the client (`req.body.role`, a JWT claim the client
    can set, a header). Roles come from your datastore, keyed on the authenticated
    identity.
    
    ## Supabase (Row-Level Security)
    
    The anon key is public **by design** — RLS policies are your access control. If
    RLS is off, the table is world-readable/writable to anyone with the key (which
    is in your client bundle).
    
    ```sql
    alter table documents enable row level security;
    
    -- deny by default, then grant precisely
    create policy "owner reads" on documents
      for select using (auth.uid() = owner_id);
    create policy "owner writes" on documents
      for insert with check (auth.uid() = owner_id);
    create policy "owner updates" on documents
      for update using (auth.uid() = owner_id) with check (auth.uid() = owner_id);
    ```
    
    - Enable RLS on **every** table holding real data; a table with RLS off and a
      policy defined is still open — the policy does nothing until RLS is enabled.
    - The `service_role` key bypasses RLS. It is server-only. Never ship it to the
      browser, never put it behind a `NEXT_PUBLIC_`/`VITE_` prefix.
    - `with check` guards writes; `using` guards reads/matches. You usually need both.
    
    ## Firebase rules
    
    ```
    // ✗ the default that ends up in production
    match /{document=**} { allow read, write: if true; }
    
    // ✓ authenticated + ownership + validation
    match /notes/{id} {
      allow read: if request.auth != null && resource.data.uid == request.auth.uid;
      allow create: if request.auth != null && request.resource.data.uid == request.auth.uid;
      allow update, delete: if request.auth != null && resource.data.uid == request.auth.uid;
    }
    ```
    
    Test rules in the emulator; a rule that fails closed is a bug you can see, a rule
    that fails open is a breach you can't.
    
    ## Sessions & tokens (quick hits)
    
    - Session cookies: `HttpOnly`, `Secure`, `SameSite=Lax` (or `Strict`). Missing
      any one of those is a finding — bare `name=value` is readable by XSS, rides
      cleartext, and is sendable cross-site.
    - JWTs: verify signature **and** `exp`; pin the algorithm (reject `alg: none` /
      `None` / `NONE` and algorithm-confusion — match case-insensitively or allow-list
      the one alg you use). Don't store them in `localStorage` if a cookie will do.
    - On logout / privilege change, invalidate server-side; a stateless JWT you can't
      revoke is a liability for sensitive apps.
    - Password hashes: slow + salted (`bcrypt` / `argon2id` / `scrypt` / PBKDF2 with
      a per-row salt). Unsalted `md5`/`sha1`/`sha256` of the password is still weak —
      rainbow-tableable — even though it is not plaintext.
    
  • skills/paranoid/references/frameworks.md17 KB8d2a3f45a3feupstream ↗
    # /hack-me framework guides
    
    Where routes and auth live in the common stacks, how to run the app locally, and
    what to probe first. Use this in the **Map** and **Prioritize** steps of
    [`/hack-me`](../../../commands/hack-me.md). The proof recipes are the same across
    frameworks — these guides just tell you where to look and how to talk to the app.
    
    Covered here: **Next.js, FastAPI, Express, Django, Ruby on Rails, Flask, Spring
    Boot, Laravel, Phoenix, Go, NestJS, ASP.NET Core.**
    
    For every stack the priority order is the same: **broken access control / IDOR →
    missing auth → injection → mass assignment → SSRF → info leaks.** Below is where
    each tends to hide per framework.
    
    ---
    
    ## Next.js (App Router / Pages Router)
    
    **Where routes live**
    - App Router: `app/**/route.ts` (Route Handlers) and `app/**/actions.ts` /
      `"use server"` functions (Server Actions). Each is *independently reachable* —
      a Server Action is a POST endpoint even if no button calls it.
    - Pages Router: `pages/api/**/*.ts`.
    - Middleware: `middleware.ts` runs at the edge; auth there does **not** cover
      Server Actions or the data layer automatically.
    
    **Where auth should be**
    - In the handler/action itself (or a wrapper it calls), re-checking the session
      server-side. `getServerSession` / `auth()` from NextAuth, Clerk's `auth()`,
      or a manual cookie/JWT check. Treat any check that only happens in a Client
      Component or in `middleware.ts` as *absent* for data endpoints.
    
    **Run it**
    - `npm run dev` (default `http://localhost:3000`). Note the actual port from the
      console; `next dev -p <port>` changes it.
    
    **Probe first**
    - Call `route.ts` handlers directly with `curl` (no cookies) — many assume "the
      UI is the only caller." → missing auth.
    - Server Actions: replay the POST the form makes, tampering ids and role/price
      fields in the body → IDOR / mass assignment.
    - `NEXT_PUBLIC_*` env vars in the client bundle → leaked secrets (grep the
      built JS / `.env`).
    - Any "fetch a URL" API route (og-image, link preview, proxy) → SSRF.
    
    ---
    
    ## FastAPI (Python)
    
    **Where routes live**
    - `@app.get(...)` / `@app.post(...)` / `APIRouter` decorators, usually under
      `app/`, `routers/`, or `api/`. `app.include_router(...)` wires them up.
    - Interactive route map for free: open `http://localhost:8000/docs` (Swagger) or
      `/openapi.json` — it enumerates every path and its params.
    
    **Where auth should be**
    - FastAPI dependencies: `Depends(get_current_user)` on the route or router.
      A route with **no** `Depends` guarding identity is public. Check that the
      dependency actually *authorizes* (ownership/role), not just authenticates.
    - Ownership must be in the query (`WHERE owner_id == current_user.id`), not an
      `if` you can forget.
    
    **Run it**
    - `uvicorn app.main:app --reload` (default `http://127.0.0.1:8000`), or
      `fastapi dev`. Confirm the port from the console.
    
    **Probe first**
    - Pull `/openapi.json`, then hit each path with no `Authorization` header →
      missing auth.
    - Endpoints taking a path/query id (`/items/{id}`) as another user → IDOR.
    - Pydantic models that accept extra fields, or `Model(**body)` /
      `.dict()`-into-update patterns → mass assignment (watch for `model_config`
      without `extra="forbid"`).
    - Raw SQL via `text("... %s" % x)` or f-strings in SQLAlchemy → SQLi.
    
    ---
    
    ## Express (Node.js)
    
    **Where routes live**
    - `app.get/post(...)`, `router.<verb>(...)` across `routes/`, `controllers/`,
      or a single `server.js`/`index.js`. Middleware chains matter: the order of
      `app.use(...)` decides whether auth runs before a handler.
    
    **Where auth should be**
    - An auth middleware (`requireAuth`, `passport.authenticate`, a JWT verifier)
      applied to the route or router. A handler registered *before* the auth
      `app.use`, or on a router that never mounts it, is public.
    - After auth, look for the authorization gap: `req.user` exists but the DB query
      doesn't scope to `req.user.id`.
    
    **Run it**
    - `npm start` / `npm run dev` / `node server.js`. Port is usually
      `process.env.PORT || 3000` — read it from the console.
    
    **Probe first**
    - Hit each route with no cookie/token → missing auth; then as the wrong user
      with someone else's id → IDOR.
    - `data: req.body`, `Object.assign(row, req.body)`, `...req.body` into an
      update → mass assignment (try smuggling `role`/`isAdmin`).
    - `db.query(\`... ${x}\`)`, `.raw()`, or `child_process.exec(\`... ${x}\`)` →
      SQL / command injection.
    - `res.send(err)` / stack traces to the client, and `cors({ origin: true,
      credentials: true })` → info leak / permissive CORS.
    
    ---
    
    ## Django (Python)
    
    **Where routes live**
    - `urls.py` (`urlpatterns`) maps paths to views in `views.py` or DRF
      `ViewSet`s/`APIView`s under an app dir. `django-admin` mounts `/admin/`.
    
    **Where auth should be**
    - `@login_required` / `LoginRequiredMixin` for authentication; **object**
      permissions are the gap — a `get_object_or_404(Model, pk=...)` with no owner
      filter is IDOR. Scope querysets: `Model.objects.filter(owner=request.user)`.
    - DRF: `permission_classes` on the view, plus `get_queryset()` scoped to
      `self.request.user`. `AllowAny` is an explicit decision, not a default.
    
    **Run it**
    - `python manage.py runserver` (default `http://127.0.0.1:8000`).
    
    **Probe first**
    - Detail/edit/delete views taking a `pk`/`slug`, requested as another user → IDOR.
    - `ModelForm`/serializer with `fields = '__all__'` → mass assignment (can set
      `is_staff`/`is_superuser`/FK owner). Serializers should list fields explicitly
      and mark server-owned ones `read_only`.
    - `.raw()` / `.extra()` / `cursor.execute(f"...")` → SQLi.
    - `mark_safe(...)` / `|safe` / `format_html` with user data → XSS.
    - `DEBUG = True` reachable → stack traces + settings leak.
    
    ## Ruby on Rails
    
    **Where routes live**
    - `config/routes.rb` (`resources :things`) → `app/controllers/*_controller.rb`
      actions. `rails routes` prints the full map.
    
    **Where auth should be**
    - A `before_action :authenticate_user!` (Devise) for authn; authorization is the
      classic Rails gap — scope through the association:
      `current_user.things.find(params[:id])`, not `Thing.find(params[:id])`.
    - Strong parameters are the mass-assignment guard:
      `params.require(:thing).permit(:title, :body)` — never `permit!`.
    
    **Run it**
    - `bin/rails server` (default `http://localhost:3000`).
    
    **Probe first**
    - `Model.find(params[:id])` without `current_user` scope → IDOR.
    - `permit!` or a permit list that includes `role`/`admin`/`user_id` → mass
      assignment / privilege escalation.
    - `where("name = '#{params[:q]}'")` / string-interpolated SQL → SQLi.
    - `html_safe` / `raw()` on user input → XSS.
    - Missing `authenticate_user!` on an admin/namespaced controller → missing auth.
    
    ## Flask (Python)
    
    **Where routes live**
    - `@app.route(...)` / `@bp.route(...)` decorators across `app.py`, a `views.py`,
      or blueprints registered with `app.register_blueprint(...)`.
    
    **Where auth should be**
    - A decorator (`@login_required` from Flask-Login, or a hand-rolled
      `before_request`) — Flask ships with **nothing** on by default, so a route
      with no decorator is fully public. Ownership is manual: scope every query to
      `current_user`, don't just check `current_user.is_authenticated`.
    
    **Run it**
    - `flask run` or `python app.py` (default `http://127.0.0.1:5000`; watch for
      `debug=True`, which serves the interactive Werkzeug console — see VAmPI in
      [`../../../examples/vampi`](../../../examples/vampi)).
    
    **Probe first**
    - Routes taking an id from the URL/args with no owner check → IDOR; unauthenticated
      → missing auth.
    - `db.session.execute(text(f"... {x}"))` / string-built SQL → SQLi.
    - `render_template_string(user_input)` or `Template(user_input)` → SSTI; user data
      in `{{ ... | safe }}` → XSS.
    - `subprocess.run(cmd, shell=True)` with user input → command injection.
    - A debug/admin route that dumps data with no `is_admin` check → broken function
      auth (this is exactly VAmPI Finding 1).
    
    ## Spring Boot (Java / Kotlin)
    
    **Where routes live**
    - `@RestController` / `@Controller` classes with `@GetMapping` / `@PostMapping`
      (etc.) methods, usually under a `controller`/`web` package.
    
    **Where auth should be**
    - Spring Security: an `HttpSecurity` config (`SecurityFilterChain`) plus
      method-level `@PreAuthorize("hasRole('ADMIN')")` / `@PreAuthorize("#id == principal.id")`.
      A `permitAll()` that's too broad, or a controller with no method security, is the
      gap. Object ownership belongs in the query or a `@PreAuthorize` SpEL check.
    
    **Run it**
    - `./mvnw spring-boot:run` or `./gradlew bootRun` (default `http://localhost:8080`).
    
    **Probe first**
    - Endpoints with a path variable id and no `@PreAuthorize`/owner check → IDOR;
      actuator or admin paths reachable → missing/again function auth.
    - `entityManager.createQuery("... " + x)` / string-concatenated JPQL/SQL → SQLi
      (use bound parameters / criteria API).
    - Binding the request body straight onto a JPA entity (`@ModelAttribute` /
      `@RequestBody Entity`) → mass assignment; use a DTO with explicit fields.
    - Thymeleaf `[(${...})]` / `th:utext` with user data → XSS.
    - Exposed Spring Boot Actuator (`/actuator/env`, `/actuator/heapdump`) → secrets
      leak.
    
    ## Laravel (PHP)
    
    **Where routes live**
    - `routes/web.php` and `routes/api.php` map paths to controller methods in
      `app/Http/Controllers`. `php artisan route:list` prints the full map.
    
    **Where auth should be**
    - The `auth` middleware on the route/group for authentication; **authorization**
      is Policies/Gates (`$this->authorize('update', $post)`) — the common gap is
      loading a model by id and returning it without a policy check. Scope through the
      relationship: `auth()->user()->posts()->findOrFail($id)`.
    - Mass assignment: rely on `$fillable` (allow-list), never `$guarded = []` plus
      `Model::create($request->all())`.
    
    **Run it**
    - `php artisan serve` (default `http://127.0.0.1:8000`), or `./vendor/bin/sail up`.
    
    **Probe first**
    - `Model::find($id)` / route-model binding with no policy → IDOR; a route outside
      the `auth` group → missing auth.
    - `DB::select("... $q")` / `whereRaw("... $q")` → SQLi (use bindings).
    - `$request->all()` into `create`/`update` with `$guarded = []` → mass assignment
      (can set `is_admin`, `role`).
    - Blade `{!! $userInput !!}` (unescaped) → XSS; `{{ }}` is safe.
    - `APP_DEBUG=true` reachable → Ignition stack traces + env leak; `.env` served.
    
    ## Phoenix (Elixir)
    
    **Where routes live**
    - `lib/<app>_web/router.ex` (`scope`/`pipe_through`) maps to controllers in
      `lib/<app>_web/controllers`. `mix phx.routes` prints the map.
    
    **Where auth should be**
    - A plug in the router pipeline (e.g. a `:require_auth` plug, or `guardian`) for
      authentication; ownership is enforced in the context function / Ecto query, not
      the controller. Scope queries by the current user
      (`from p in Post, where: p.user_id == ^user.id`).
    
    **Run it**
    - `mix phx.server` (default `http://localhost:4000`).
    
    **Probe first**
    - A controller action taking `params["id"]` and calling a context `get_*!` with no
      owner scope → IDOR; an action outside the authed pipeline → missing auth.
    - `Repo.query!("... #{q}")` / string-built SQL → SQLi (use parameterized
      `from`/`^`).
    - `raw(user_input)` in HEEx/EEx templates → XSS (`<%= %>` auto-escapes).
    - `cast/3` including sensitive fields (e.g. `:role`, `:admin`) in the changeset
      permit list → mass assignment.
    
    ## Go (net/http, chi, gin, echo)
    
    **Where routes live**
    - `http.HandleFunc` / `mux.HandleFunc`, or a router (`chi`, `gin`, `echo`)
      registering handlers, usually in `main.go` or a `handlers/` package.
    
    **Where auth should be**
    - Middleware wrapping the mux/router that populates a user into the request
      `context.Context`; handlers read `r.Context()`. A handler registered outside
      the authed middleware chain is public.
    - Ownership: put the user id in the query
      (`WHERE id = $1 AND owner_id = $2`), not an `if` after the row loads.
    
    **Run it**
    - `go run .` / `go run ./cmd/server`. Port is usually a flag or
      `os.Getenv("PORT")` — read the startup log.
    
    **Probe first**
    - Handlers reading `chi.URLParam(r,"id")` / `r.PathValue("id")` with no owner
      check → IDOR; call them unauthenticated → missing auth.
    - `db.Query(fmt.Sprintf("... %s", x))` → SQLi (use `db.Query("... $1", x)` /
      placeholders).
    - `exec.Command("sh","-c", userStr)` → command injection (pass args, not a shell
      string).
    - `text/template` (instead of `html/template`) rendering to HTML → XSS.
    - `json.Unmarshal` into a struct with server-owned fields that the client can set
      → mass assignment.
    
    ## NestJS (Node.js / TypeScript)
    
    **Where routes live**
    - `@Controller('prefix')` classes decorated with `@Get(...)`, `@Post(...)`, `@Patch(...)`, `@Delete(...)` across `*.controller.ts` files under `src/`.
    - Controllers are registered in `@Module({ controllers: [...] })` arrays (`*.module.ts`).
    - Global route prefixes (`app.setGlobalPrefix('api')`) and global pipes/guards in `main.ts` or `app.module.ts`.
    - Interactive route discovery: if `@nestjs/swagger` is configured, inspect `http://localhost:3000/api` or `/docs`.
    
    **Where auth should be**
    - `@UseGuards(AuthGuard('jwt'))` or custom authorization guards (`@UseGuards(RolesGuard)`) placed at the controller class or method level, or wired globally via `APP_GUARD`.
    - Routes without an explicit guard (or where `@Public()` / `@SetMetadata('isPublic', true)` bypasses global guards) are public.
    - In handlers, extract identity via `@Req() req` or custom decorators (`@CurrentUser()`). Authorization / IDOR defense must be scoped in the service/query layer (e.g. `where: { id, userId }` in TypeORM/Prisma), not merely verifying that the caller holds any valid JWT.
    
    **Run it**
    - `npm run start:dev` (default `http://localhost:3000`).
    
    **Probe first**
    - Methods with an id param (`@Param('id')`) calling `service.update(id, dto)` or `service.remove(id)` without validating ownership against `user.id` → IDOR.
    - Request DTOs without `new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })` registered globally: client can pass extra fields (`role`, `isAdmin`, `isVerified`) directly into TypeORM/Prisma `.save()` or `.update()` → mass assignment.
    - Raw database queries: TypeORM `repository.query("... " + input)` or `createQueryBuilder().where("... " + input)`, Prisma `$queryRawUnsafe(...)` → SQLi.
    - Calling endpoints without `Authorization: Bearer <token>` or tampering JWT payload claims → missing / broken auth.
    - Bypassing built-in sanitization with raw Express `@Res() res` / `@Next() next` → reflected XSS or header injection.
    
    ## ASP.NET Core (C# / .NET)
    
    **Where routes live**
    - Controller-based APIs: `[ApiController]` and `[Route("[controller]")]` classes in `Controllers/*Controller.cs`, with `[HttpGet]`, `[HttpPost("{id}")]`, etc.
    - Minimal APIs: `app.MapGet(...)`, `app.MapPost(...)`, and route groups (`app.MapGroup("/api")`) in `Program.cs`.
    - Route discovery: Swagger/OpenAPI UI at `http://localhost:5000/swagger` or `/openapi/v1.json` when `app.UseSwagger()` is enabled.
    
    **Where auth should be**
    - `[Authorize]` attribute on the controller class or action method, or `.RequireAuthorization()` chained onto Minimal API route endpoints.
    - Fallback/default policy in `Program.cs` (`options.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()`). Endpoints decorated with `[AllowAnonymous]` bypass all authorization checks.
    - Ownership / IDOR: EF Core `_context.Items.FindAsync(id)` loads by primary key alone without tenant/owner filtering. Always query with ownership predicate: `_context.Items.Where(x => x.Id == id && x.UserId == currentUserId).FirstOrDefaultAsync()`.
    
    **Run it**
    - `dotnet run` or `dotnet watch` (default `http://localhost:5000` / `https://localhost:5001`). Port configuration in `Properties/launchSettings.json` or `ASPNETCORE_URLS`.
    
    **Probe first**
    - Endpoints taking `{id}` route parameters without user scoping on the EF Core query → IDOR; endpoints missing `[Authorize]` or carrying `[AllowAnonymous]` → missing auth.
    - Direct entity binding: binding request payload directly to an EF Core entity model (`public async Task<IActionResult> Post(User user)`) instead of an input DTO / record → mass assignment / overposting (can overwrite `IsAdmin`, `RoleId`, etc.).
    - String-interpolated raw SQL: `_context.Database.ExecuteSqlRaw($"SELECT ... {input}")` or `_context.FromSqlRaw($"... {input}")` → SQLi (use `ExecuteSqlInterpolated` or parameterized queries).
    - Razor views (`.cshtml`) using `@Html.Raw(userInput)` instead of standard `@userInput` auto-encoding → XSS.
    - Unhandled exceptions returning detailed stack traces or database connection strings if `app.UseDeveloperExceptionPage()` is active in production/staging environments → info leak.
    
    ## Any stack — the constants
    
    - **Base URL + two accounts.** Get the real port from the run console; ask for an
      attacker and a victim test account (local dev login only).
    - **Enumerate before probing.** A route list (source grep, `/docs`,
      `/openapi.json`, the router file) turns guessing into checking.
    - **One class at a time**, minimal request, real request/response as proof, then
      patch the root cause and replay the exact request to confirm it now fails.
    
  • skills/paranoid/references/injection.md6.5 KBfd6415f40297upstream ↗
    # Injection, SSRF & untrusted input
    
    The unifying rule: **never mix untrusted data into a command language by
    concatenation.** SQL, shell, HTML, file paths, and URLs are all languages an
    attacker can escape out of if you build strings. Use the structured API instead.
    
    ## SQL injection
    
    ```js
    // ✗
    db.query(`SELECT * FROM users WHERE email = '${email}'`);
    db.query("... WHERE id = " + id);
    // ✓ parameterized
    db.query('SELECT * FROM users WHERE email = $1', [email]);        // pg
    db.execute('SELECT * FROM users WHERE email = ?', [email]);        // mysql2
    ```
    
    ```python
    cur.execute("SELECT * FROM users WHERE email = %s", (email,))      # ✓
    cur.execute(f"SELECT * FROM users WHERE email = '{email}'")        # ✗
    ```
    
    - ORMs (Prisma, Drizzle, SQLAlchemy, ActiveRecord) parameterize for you —
      **until** you drop to `$queryRawUnsafe`, `.raw()`, `text()` with an f-string,
      or `find_by_sql`. Those re-open the door; parameterize there too.
    - Column/table names can't be parameterized — allow-list them against a fixed set,
      never interpolate user input into identifiers.
    
    ## Command injection
    
    ```js
    // ✗ user input in a shell string
    exec(`convert ${req.file.path} out.png`);
    // ✓ no shell, argv array
    execFile('convert', [req.file.path, 'out.png']);
    ```
    
    ```python
    subprocess.run(["convert", path, "out.png"])        # ✓ list, no shell
    subprocess.run(f"convert {path} out.png", shell=True) # ✗
    ```
    
    - Avoid `shell=True` / the string form of `exec`. Pass an argv array so the OS
      never re-parses metacharacters (`; | & $() ` `` ` ``).
    - `eval`, `Function()`, `exec()`, `pickle.loads`, `yaml.load` (use `safe_load`),
      and unbounded template engines are all code-execution sinks — never on user input.
    
    ## XSS
    
    ```jsx
    // ✗
    <div dangerouslySetInnerHTML={{ __html: comment }} />
    element.innerHTML = comment;   // vanilla
    <div v-html="comment" />       // vue
    // ✓
    <div>{comment}</div>           // React auto-escapes
    element.textContent = comment;
    ```
    
    - Default to text rendering; frameworks escape `{expr}` for you — the danger is
      the explicit "raw HTML" escape hatch.
    - If you genuinely must render user HTML (rich text), sanitize with **DOMPurify**
      (client) or a server sanitizer — allow-list tags/attributes, never a blocklist.
    - Also escape in non-HTML sinks: attribute values, `href`/`src` (block
      `javascript:`), inline `<script>` JSON (use `JSON.stringify` + escape `<`).
    - Defense in depth: a `Content-Security-Policy` that forbids inline script turns
      many XSS bugs into non-events.
    
    ## Open redirect
    
    A `?next=` (or `return_to`) value that lands in a `Location` header is an open
    redirect unless you allow-list it. Reject CR/LF too: `urlparse` strips them
    from the path, so `/dashboard\r\nLocation: https://evil.example` looks like a
    relative URL while the raw string injects a second header (response splitting).
    
    ```python
    # ✗
    return redirect(request.args.get("next") or "/")
    # ✓ only a same-site relative path, or http(s) on your own host;
    #   reject CR/LF and protocol-relative "//host"
    ```
    
    ## Path traversal
    
    ```js
    // ✗ ../../../etc/passwd
    fs.readFile(path.join(UPLOAD_DIR, req.query.name));
    // ✓ strip directory components, then confirm containment
    const name = path.basename(req.query.name);
    const full = path.resolve(UPLOAD_DIR, name);
    if (!full.startsWith(path.resolve(UPLOAD_DIR) + path.sep)) throw new Error('bad path');
    // symlinks escape a lexical check — resolve the real path when the dir may hold them
    const real = await fs.promises.realpath(full);
    if (!real.startsWith(path.resolve(UPLOAD_DIR) + path.sep)) throw new Error('bad path');
    ```
    
    - `path.basename` drops `../`, and the `startsWith(resolvedDir + sep)` check
      rejects absolute paths and lexical `../` escapes. It does **not** catch
      symlinks: `path.resolve` is a pure string operation that never touches disk, so
      a symlink *inside* `UPLOAD_DIR` pointing at `/etc` still passes it. When the
      directory can contain symlinks (a prior upload, an extracted archive), resolve
      the real path with `fs.realpath` and check containment on that — as the second
      check above does. (`realpath` needs the file to exist, which a read already
      requires; for a write target, realpath the *directory* instead.) Never serve
      files by a raw client path.
    - For uploads: allow-list extensions/MIME, generate your own filename, store
      outside the web root, and cap the size.
    
    ## Mass assignment
    
    ```js
    // ✗ user can set role, credits, userId…
    await db.user.update({ where: { id }, data: req.body });
    // ✓ parse + allow-list
    const { name, bio } = updateSchema.parse(req.body);
    await db.user.update({ where: { id }, data: { name, bio } });
    ```
    
    Validate every request body through a schema (zod, pydantic, valibot) at the
    edge. It gives you allow-listing, type coercion, and length/format bounds in one
    move — and rejects the 10 MB blob and the negative id for free.
    
    ## SSRF (server-side request forgery)
    
    When the server fetches a URL the user supplied (link previews, webhooks
    "test", image import, PDF render):
    
    ```js
    const url = new URL(input);
    if (!['http:', 'https:'].includes(url.protocol)) throw new Error('scheme');
    // resolve DNS and reject private / link-local / loopback targets
    const { address } = await dns.lookup(url.hostname);
    if (isPrivate(address)) throw new Error('blocked host'); // 10/8,172.16/12,192.168/16,127/8,169.254/16,100.64/10,::1,fc00::/7
    const res = await fetch(url, { redirect: 'manual' });    // don't auto-follow to an internal 302
    ```
    
    - The prize is usually cloud metadata (`169.254.169.254`) or internal admin
      services. Block by resolved IP, not by hostname string (DNS rebinding).
    - Pin the validated IP at fetch time (or connect to the IP directly with a `Host` header) to eliminate the TOCTOU gap between DNS validation and request execution.
    - Allow-list hosts if you can (you rarely need to fetch *arbitrary* URLs).
    - Disable auto-redirects, or re-validate the target on each hop.
    
    ## Deserialization & other sinks
    
    Untrusted input into `pickle`, Java/Ruby native deserialization, `yaml.load`,
    XML parsers with external entities (XXE), or regexes built from user input
    (ReDoS) are all injection cousins. Same rule: parse with a safe, structured
    loader; never let input choose what code runs.
    
    ```python
    # ✗ nested quantifiers — `'A'*36+'!'` never matches and backtracks exponentially
    re.match(r'^([A-Za-z0-9]+)+$', username)
    # ✓ same language, linear in the input length
    username.isascii() and username.isalnum()
    # or re.fullmatch(r'[A-Za-z0-9]+', username)
    ```
    
    Bound the input if you must keep a complex pattern. Don't measure "it was fast
    on happy-path strings" — that's how ReDoS ships.
    
  • skills/paranoid/references/secrets-config.md4.5 KB5be46cbdbd6fupstream ↗
    # Secrets, config & the client boundary
    
    Two failure modes here: secrets that reach the browser, and security decisions
    made on a device the user controls. Both come from forgetting that **everything
    shipped to the client is public** — bundles are readable, requests are editable,
    `localStorage` is inspectable.
    
    ## Secrets live server-side, in env, never in code
    
    ```ts
    // ✗
    const stripe = new Stripe('sk_live_51J...');
    // ✓
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
    ```
    
    - Never hardcode keys, tokens, or connection strings. Read from `process.env` /
      `os.environ` and fail loudly at boot if a required one is missing.
    - `.env` is **git-ignored**; commit a `.env.example` with empty values instead.
    - If a secret was ever committed, it is compromised — rotate it. Removing it in a
      later commit does not help; it's in history. Say this to the user plainly.
    - Never put a secret in an error message, an API response, or a log line.
    
    ### The client-env-prefix trap
    
    Bundlers inline any variable with a public prefix into the browser bundle:
    
    - Next.js: `NEXT_PUBLIC_*`
    - Vite: `VITE_*`
    - Create React App: `REACT_APP_*`
    - Expo: `EXPO_PUBLIC_*`
    
    A `NEXT_PUBLIC_STRIPE_SECRET_KEY` is a public secret — a contradiction that ships
    constantly. Only publishable/anon keys belong under these prefixes. When you add
    one, ask: "is it fine for this to be in HTML anyone can view?" If not, it's a
    server variable and the code that uses it must run on the server.
    
    ### The other direction: don't strip the keys that are meant to ship
    
    This is the failure mode of being *too* careful, and it is measured, not
    hypothetical: in this project's own benchmark, the one functional test the
    paranoid condition failed (and the baseline passed) was a client-config task
    where it dropped `STRIPE_PUBLISHABLE_KEY` along with the real secrets. The
    guidance above, followed without this counterweight, filters until the feature
    breaks. A security fix that breaks payments is not a fix.
    
    Some credentials are **designed** to be public and are load-bearing in the
    browser. Removing them doesn't harden anything — it just breaks checkout, auth,
    maps, or analytics:
    
    | Meant to ship | Never ships |
    |---|---|
    | Stripe **publishable** key (`pk_live_…`, `pk_test_…`) | Stripe secret key (`sk_live_…`) |
    | Supabase **anon** key | Supabase `service_role` key |
    | Firebase web config, including `apiKey` | Firebase Admin SDK service-account JSON |
    | Sentry/PostHog **public DSN** or project key | Sentry auth token |
    | Google Maps **browser** key (restrict by referrer) | Google service-account key |
    | Algolia **search-only** key | Algolia admin key |
    
    The rule: **decide by what the credential is, not by what its name looks like.**
    `SECRET`, `KEY` and `TOKEN` in a variable name are a prompt to check, not a
    verdict — `STRIPE_PUBLISHABLE_KEY` and `SESSION_SECRET` both contain one of
    those words and belong on opposite sides of the boundary. If you can't tell what
    a credential is, say so and ask, rather than silently dropping it.
    
    Firebase's `apiKey` is the classic confusion: it is an identifier, not an
    authenticator, and Firebase documents it as public. Access is controlled by
    security rules, so removing the key breaks the app without securing anything.
    
    ## The client enforces nothing
    
    Anything the client sends can be forged with curl. So:
    
    - **Prices, totals, discounts:** the server derives them from ids, never accepts
      amounts from the body.
    - **Roles / permissions / `isAdmin`:** read from your datastore by authenticated
      identity, never from the request.
    - **Quotas, limits, feature gates:** re-checked server-side even if the UI hides
      the control.
    - **Validation:** client-side validation is UX; the server validates again
      because the client can be bypassed entirely.
    
    ```ts
    // ✗ trusting the client's claim
    if (req.body.isPremium) unlock(feature);
    // ✓ server is the source of truth
    const user = await db.user.findUnique({ where: { id: req.user.id } });
    if (user.plan === 'premium') unlock(feature);
    ```
    
    A useful mental test: **"If the attacker writes the client, does this still
    hold?"** If not, move the check to the server.
    
    ## Config hygiene
    
    - Separate keys per environment (dev/staging/prod); a leaked dev key shouldn't
      touch prod data.
    - Least privilege on the key itself — a read-only key for a read-only job beats a
      god key everywhere.
    - Scan before you push: `git secrets`, `gitleaks`, or `trufflehog` in a
      pre-commit hook catches the accident before it's permanent.
    - Debug/verbose flags off in production — they turn into #10 (verbose errors).
    
  • skills/paranoid/references/vibe-top-10.md5.7 KBa679ab7dd023upstream ↗
    # The vibe-coded top 10
    
    The vulnerability classes that AI-generated and "vibe-coded" apps actually ship,
    ranked by how often they show up. Each entry: **what the agent does wrong**, how
    to **spot** it, and the **fix pattern**. This is the map — the four topic files
    (`auth-access`, `secrets-config`, `injection`, `apis-webhooks`) hold the detail.
    
    ---
    
    ### 1. Broken access control / IDOR
    
    The single most common one. The endpoint authenticates the user ("are you logged
    in?") but never authorizes the *object* ("is this row yours?").
    
    ```js
    // ✗ any logged-in user can read any invoice
    app.get('/api/invoices/:id', auth, async (req, res) => {
      const inv = await db.invoice.findUnique({ where: { id: req.params.id } });
      res.json(inv);
    });
    // ✓ scope every lookup to the caller
    const inv = await db.invoice.findFirst({
      where: { id: req.params.id, userId: req.user.id },
    });
    if (!inv) return res.sendStatus(404);
    ```
    
    **Spot:** any query keyed on a client-supplied id without the caller's id in the
    `where`. **Fix:** ownership in the query itself, `404` (not `403`) on miss.
    → `auth-access.md`
    
    ### 2. Missing authentication on endpoints
    
    New route ships with no auth because "the frontend never calls it that way."
    Attackers call it directly.
    
    **Spot:** route/handler/server-action with no auth middleware or session check;
    admin or internal routes guessable by name. **Fix:** deny by default — a shared
    `requireAuth` wrapper, and public routes are the annotated exception.
    → `apis-webhooks.md`
    
    ### 3. Trusting the client
    
    Permission checks, prices, roles, or feature gates enforced only in the browser;
    the server accepts whatever arrives.
    
    ```js
    // ✗ client sends the price
    await charge(req.body.amountCents);
    // ✓ server derives it
    const price = await priceFor(req.body.sku);
    ```
    
    **Spot:** `if (user.isAdmin)` in frontend code with no server twin; amounts,
    roles, or `isAdmin` read from the request body. **Fix:** the server re-derives
    and re-checks everything that matters. → `secrets-config.md`
    
    ### 4. Injection (SQL / command / XSS / path)
    
    String-built queries and commands, unescaped HTML, user input in file paths.
    
    ```js
    db.query(`SELECT * FROM users WHERE email = '${email}'`); // ✗ SQLi
    db.query('SELECT * FROM users WHERE email = $1', [email]); // ✓
    element.innerHTML = comment;                               // ✗ XSS
    element.textContent = comment;                             // ✓
    fs.readFile(`/data/${req.query.name}`);                    // ✗ ../../etc/passwd
    ```
    
    **Spot:** template strings / `+` around SQL, shell, HTML, or paths; `innerHTML`,
    `dangerouslySetInnerHTML`, `v-html`; `exec`/`eval` with dynamic input. **Fix:**
    parameterize, use `textContent`, pass argv arrays not shell strings, allow-list
    and `path.basename` file names. → `injection.md`
    
    ### 5. Insecure database / BaaS rules
    
    Supabase table with RLS off; Firebase rules `allow read, write: if true`. The
    API key is public by design, so the rules *are* the security — and they're open.
    
    **Spot:** RLS disabled on a table with real data; any rule that resolves to
    `true`; a service-role key used from the client. **Fix:** RLS on, deny by
    default, policies scoped to `auth.uid()`. → `auth-access.md`
    
    ### 6. SSRF (server fetches a user-supplied URL)
    
    Webhook testers, link previews, image proxies, "import from URL." The server
    fetches attacker-controlled URLs and can be pointed at `169.254.169.254`
    (cloud metadata) or internal services.
    
    **Spot:** `fetch`/`axios`/`requests` where the URL comes from the user. **Fix:**
    allow-list schemes and hosts, resolve DNS and block private/link-local ranges,
    disable redirects to them. → `injection.md`
    
    ### 7. Leaked secrets
    
    API keys hardcoded in source, `.env` committed, keys inlined into the client
    bundle (`NEXT_PUBLIC_…`, `VITE_…`), or secrets echoed in error responses/logs.
    
    **Spot:** high-entropy strings in source; `.env` tracked by git; secrets under a
    client-exposed env prefix; keys in log lines. **Fix:** env only, server only,
    rotate anything that was committed, `.gitignore` the env file.
    → `secrets-config.md`
    
    ### 8. Mass assignment / missing validation
    
    The whole request body is trusted and spread into a DB update, letting a user
    set fields they shouldn't (`role`, `isAdmin`, `credits`, `userId`).
    
    ```js
    await db.user.update({ where: { id }, data: req.body }); // ✗ sets role too
    ```
    
    **Spot:** `...req.body`, `Object.assign(record, req.body)`, `data: req.body`.
    **Fix:** parse the body through a schema (zod/pydantic) and pass only the
    allow-listed fields. → `injection.md`
    
    ### 9. Unverified webhooks & no rate limiting
    
    Payment/CI webhooks whose signature is never checked (anyone can POST a fake
    "payment succeeded"); auth and reset endpoints with no throttle (credential
    stuffing, enumeration).
    
    **Spot:** a webhook handler reading `req.body` with no signature verification;
    login/OTP/reset routes with no rate limit. **Fix:** verify the provider
    signature against the raw body; rate-limit sensitive routes.
    → `apis-webhooks.md`
    
    ### 10. Verbose errors, PII logs, permissive CORS
    
    Stack traces and SQL errors returned to the client; emails/tokens written to
    logs; `Access-Control-Allow-Origin: *` alongside credentials; missing security
    headers.
    
    **Spot:** `res.send(err)` / `err.stack` to client; PII in `console.log`; CORS
    reflecting any origin with `credentials: true`. **Fix:** generic client errors +
    server-side detail, redact logs, CORS allow-list, set the standard headers.
    → `apis-webhooks.md`
    
    ---
    
    **Why these ten:** they line up with OWASP's top categories (broken access
    control and injection lead there too), and with what security benchmarks find
    LLMs produce — functionally correct backends that are still exploitable about
    half the time. They are deliberately concrete: an agent can check for every one
    of them mechanically.
    
  • skills/paranoid/checklists/pre-commit.md1.6 KBc1bf3af4755dupstream ↗
    # Pre-commit security gate
    
    Run this before committing a feature that crossed any trust boundary. Seven
    questions. If code changed touches auth, data access, input, secrets, outbound
    requests, or endpoints and you can't answer "yes" (or "n/a"), fix it first.
    
    Report the result compactly, e.g.:
    
    > paranoid gate: 1 ✓ · 2 ✓ · 3 ✓ · 4 ⚠ fixed (added ownership check) · 5 ✓ · 6 n/a · 7 ✓
    
    1. **Access control** — Does every lookup/mutation on a client-supplied id scope
       to the authenticated caller (ownership/membership, not just "logged in")?
    
    2. **Auth coverage** — Is every new route/handler/action authenticated and
       authorized by default, with any public one a deliberate, marked exception?
    
    3. **Client trust** — Are all prices, roles, permissions, and gates enforced on
       the server? No secret or security decision shipped to the browser?
    
    4. **Injection** — Is all SQL/shell/HTML/path/URL construction parameterized,
       escaped, or allow-listed — no string-built queries, `innerHTML`, or `shell=True`
       on user input?
    
    5. **Validation** — Does every request body/query pass through a schema with
       field allow-listing and size/length/range bounds (no `data: req.body`)?
    
    6. **Untrusted I/O** — Are webhooks signature-verified, sensitive routes
       rate-limited, and server-side fetches of user URLs SSRF-guarded?
    
    7. **Leakage** — Errors generic to clients, no secrets/PII in logs or responses,
       CORS an allow-list, security headers set, responses free of extra DB fields?
    
    If the app is running locally and the user wants proof rather than a checklist,
    escalate to the **hack-me** workflow (`commands/hack-me.md`).
    
  • README.mdupstream ↗

    Contents are not inlined in the catalog. Open the upstream file.

Plain-text copy, no login: /skill-bundles/kulchankas-paranoid-skills-paranoid.md

// REPO STATS

48 stars