# paranoid

Adopt this skill: https://proskills.md/skills/other/kulchankas-paranoid-skills-paranoid

Page: https://proskills.md/skills/other/kulchankas-paranoid-skills-paranoid
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skills/paranoid/SKILL.md
size: 6049
sha: 00a7df24ab03cf3b7e95960b117845eee30e9175
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/SKILL.md

```text
---
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.
```

## File: skills/paranoid/references/apis-webhooks.md
size: 4349
sha: 099ed0dc3319bda5e94aa3666c425f84a58f6eff
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/references/apis-webhooks.md

````text
# 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.
````

## File: skills/paranoid/references/auth-access.md
size: 4560
sha: 99ddaa8701604081987c4c3cf6fe1831dd955cb8
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/references/auth-access.md

````text
# 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.
````

## File: skills/paranoid/references/frameworks.md
size: 17318
sha: 8d2a3f45a3fe1940d811776a51fe34803ad16d28
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/references/frameworks.md

```text
# /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.
```

## File: skills/paranoid/references/injection.md
size: 6675
sha: fd6415f402977b67e4244768a7c13d581c9d75cc
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/references/injection.md

````text
# 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.
````

## File: skills/paranoid/references/secrets-config.md
size: 4602
sha: 5be46cbdbd6f62d96310742047d8f6d53b2dda4e
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/references/secrets-config.md

````text
# 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).
````

## File: skills/paranoid/references/vibe-top-10.md
size: 5854
sha: a679ab7dd023aca04b37a0103c9f256493bcf559
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/references/vibe-top-10.md

````text
# 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.
````

## File: skills/paranoid/checklists/pre-commit.md
size: 1664
sha: c1bf3af4755da1e299f071bef085781dc4648203
upstream: https://github.com/kulchankas/paranoid/blob/main/skills/paranoid/checklists/pre-commit.md

```text
# 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`).
```

## File: README.md
upstream: https://github.com/kulchankas/paranoid/blob/main/README.md

````text
<p align="center">
  <img src="assets/banner.svg" alt="paranoid — your app is guilty until proven secure" width="100%">
</p>

<p align="center">
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-1f6feb" alt="MIT license"></a>
  <a href="https://github.com/kulchankas/paranoid/actions/workflows/benchmark.yml"><img src="https://github.com/kulchankas/paranoid/actions/workflows/benchmark.yml/badge.svg" alt="benchmark self-test"></a>
  <img src="https://img.shields.io/badge/runs%20in-Claude%20Code%20·%20Codex%20·%20Cursor-1f6feb" alt="Runs in Claude Code, Codex, Cursor">
  <img src="https://img.shields.io/badge/proofs-localhost%20only-3fb950" alt="Localhost-only proofs">
  <a href="https://github.com/kulchankas/paranoid/stargazers"><img src="https://img.shields.io/github/stars/kulchankas/paranoid?style=social" alt="GitHub stars"></a>
</p>

# 🕵️ 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.

<p align="center">
  <img src="assets/hack-me-demo.svg" alt="hack-me finds, proves, patches and re-verifies four real vulnerabilities in a running app" width="760">
</p>

## Quickstart

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

```bash
/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:

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

**Codex, Cursor, or any agent that reads skills:**

```bash
npx skills add kulchankas/paranoid/skills/paranoid
```

Copy [`commands/hack-me.md`](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**](https://github.com/erev0s/VAmPI)
— a well-known third-party vulnerable API — with nothing but its URL, `/hack-me`
found, proved, patched and re-verified **six** real bugs:

<p align="center">
  <img src="assets/vampi-receipts.svg" alt="hack-me finds, proves, patches and re-verifies six real vulnerabilities in OWASP VAmPI" width="760">
</p>

| # | Finding | OWASP API | Status |
|---|---------|:--:|:--:|
| 1 | Unauth `/users/v1/_debug` dumps every password | API3/5 | **401/403** |
| 2 | Read any user's private book secret (BOLA) | API1 | **404** |
| 3 | Register with `admin:true` → privilege escalation | API6 | **admin=false** |
| 4 | Change any user's password (account takeover) | API1 | **victim untouched** |
| 5 | SQLi in user lookup (UNION-dumps passwords) | API8 | **404** |
| 6 | Debugger + stack traces exposed | API7 | **clean 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`](examples/vampi/HACKME_REPORT.md).

### On a demo app, start to finish

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

| # | Found by probing the API | Class | After patch |
|---|---|---|:--:|
| 1 | Any user reads any invoice | IDOR / broken object auth | **404** |
| 2 | `/admin/users` open to anyone logged in | broken function auth | **403** |
| 3 | `/search?email=` SQL injection (dumped passwords) | SQLi | **`[]`** |
| 4 | `/profile` accepts `is_admin` | mass assignment → privesc | **400** |

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`](examples/ledgerlite/HACKME_REPORT.md).

### On a shop with no exploit menu

[DVWA](examples/dvwa) lists its bug categories in the navigation, so that run
tests the fix loop, not discovery. [VulnShop](https://github.com/dev-abhrajit/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:

| # | Finding | Status |
|---|---------|:--:|
| 1 | `/products?id=` dumps every password | **empty listing** |
| 2 | Login bypass to an admin session | **no session** |
| 3 | Search reflects raw HTML | **escaped** |
| 4 | Any logged-in user opens `/admin` | **403** |
| 5 | Profile save with no CSRF token | **400** |

The upstream README names those classes, so this is not a fully blind-to-the-repo
find. Receipts: [`examples/vulnshop/HACKME_REPORT.md`](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`](skills/paranoid/references/frameworks.md). Guardrails
apply throughout; see [Scope & ethics](#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/`](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:

| Model | Tasks | Exploit rate **without** skill | **with** skill | Effect |
|---|---|:--:|:--:|:--:|
| Opus | **all 22 classes, blinded** | **0%** | **0%** | **none** |
| Opus | 3 isolated functions | 0% | 0% | none |
| Fable 5.1 | 3 isolated functions | 0% | 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 
````
