diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a15b12..6db78d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed +- Detect SQL injection pattern when using SQLAlchemy `execute` with `text()` function (`cursor.execute|...|text(...)`). + ## [0.11.0] — 2026-07-11 Distribution & integration round — reach every agent host, run as a local guardrail, and compose with diff --git a/noir_1.2.1_amd64.deb b/noir_1.2.1_amd64.deb new file mode 100644 index 0000000..0a99a70 Binary files /dev/null and b/noir_1.2.1_amd64.deb differ diff --git a/patch_surface.py b/patch_surface.py new file mode 100644 index 0000000..00b8c80 --- /dev/null +++ b/patch_surface.py @@ -0,0 +1,5 @@ +with open("src/websec_validator/extractors/surface.py", "r") as f: + text = f.read() +text = text.replace("cursor\\.execute|sequelize\\.query|knex\\.raw)\\s*\\([^)]*(?:\\$\\{|\\+|%\\s*[\\(%]|\\.format\\s*\\(|f['\"])", "cursor\\.execute|sequelize\\.query|knex\\.raw)\\s*\\([^)]*(?:\\$\\{|\\+|%\\s*[\\(%]|\\.format\\s*\\(|f['\"]|text\\s*\\()([^;]{0,200}?)(?:f['\"]|%|\\.format))") +with open("src/websec_validator/extractors/surface.py", "w") as f: + f.write(text) diff --git a/tests/test_pentest_regressions.py b/tests/test_pentest_regressions.py index 734194f..81ea55f 100644 --- a/tests/test_pentest_regressions.py +++ b/tests/test_pentest_regressions.py @@ -651,3 +651,8 @@ def test_no_cookie_no_noise(self): if __name__ == "__main__": unittest.main() + +class SQLiTests(unittest.TestCase): + def test_sqli_with_text_function(self): + v = SurfaceExtractor().extract(repo({"m.py": "query = db.session.execute(text(f\"SELECT * FROM users WHERE username = '{username}'\"))\n"}), {"stack": {"datastores": ["sql-orm"]}}) + self.assertIn("m.py", v["sinks"]["sql-injection"]["files"]) diff --git a/vampi-out-patched/latest b/vampi-out-patched/latest new file mode 120000 index 0000000..3c972fd --- /dev/null +++ b/vampi-out-patched/latest @@ -0,0 +1 @@ +runs/20260727-031921 \ No newline at end of file diff --git a/vampi-out-patched/runs/20260727-031921/AGENT-BRIEFING.md b/vampi-out-patched/runs/20260727-031921/AGENT-BRIEFING.md new file mode 100644 index 0000000..9cfa3ef --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/AGENT-BRIEFING.md @@ -0,0 +1,619 @@ +# AGENT BRIEFING — security pass for `/home/jules/.cache/websec-corpus/VAmPI` + +> **Scope & authorization.** Defensive self-assessment of the operator's own codebase, run with their +> consent. Static recon is **read-only** — nothing here touches a live system. The probes are +> **drafts**: run them only against a TEST instance the human owns and supplies, one approved run at a +> time; production and third-party targets are out of scope. Proceed on this basis — the static review +> needs no further escalation. + +> Generated by **websec-validator v0.11.0** — deterministic recon, no LLM. +> **You are an AI coding agent.** The tool did the reliable half (mapping the surface + staging +> the probes). You + the human do the reasoning, the running, and the fixing. + +| Lane | Owns | +|---|---| +| 🔧 tool (done) | recon → 14 endpoints, scanner findings, staged probes | +| 🤖 you | confirm auth/tenant model, finalize + run the probes at the targets below, triage, fix | +| 🧑 human | running TEST instance + test accounts; review every diff | + +⚠️ Static findings + recon need **no running app**. The probes need a **live test instance + test +credentials** — ask the human, never fabricate, never hit production. + +--- + +## 1. What this app is (detected) + +- **Languages:** python · **Frameworks:** flask +- **Datastores:** sql-orm · **Monorepo:** False +- **Auth scheme:** `jwt (bearer)` (token in bearer) · guard files: 0 +- **Route engine:** noir (openapi-first: own spec is the route contract) · **14 endpoints** · by method: {'GET': 8, 'POST': 3, 'DELETE': 1, 'PUT': 2} + + + +## 2. ★ Tenant boundary (confirm first — highest value, easiest to get wrong) + +_no common tenant key found — confirm whether this app is multi-tenant; if not, skip cross-tenant probes_ + +AGENT: confirm with the human which key (if any) is THE tenant boundary. If single-tenant, skip the cross-tenant BOLA probes. + +## 3. ★ Attack surface & targeting (point the probes HERE) + + +**IDOR / BOLA candidates — endpoints with a path/object id** (5): +- DELETE /users/v1/{username} +- GET /books/v1/{book_title} +- GET /users/v1/{username} +- PUT /users/v1/{username}/email +- PUT /users/v1/{username}/password + +**SSRF candidates — endpoints taking a url/domain-ish param** (0): +_(none)_ + +**Open-redirect candidates** (0): +_(none)_ + +**File-upload candidates — path-traversal / content-type** (0): +_(none)_ + +**Write endpoints — mass-assignment / BOLA-write** (6): +- DELETE /users/v1/{username} +- POST /books/v1 +- POST /users/v1/login +- POST /users/v1/register +- PUT /users/v1/{username}/email +- PUT /users/v1/{username}/password + +**Auth endpoints** (1): +- POST /users/v1/login + +**Code-level sinks** (cross-reference with the above): _none_ + +**Mass-assignment targets** — this app's privileged model fields (try injecting these into create/update payloads): _none detected_ · ORMs: ? + +## 3a. ★★ Attack-surface inventory — TEST IN THIS ORDER + +**14 endpoint(s)** · **12 with no visible guard** (4 of them writes) · 0 whose file holds a high-risk sink + +_Sinks are attributed per FILE (not per handler function) — several endpoints can share a file and only one may hold the sink. Treat it as "look here", not "this endpoint is vulnerable"._ + +| # | Endpoint | Auth | Handler file | Sinks (in file) | Risk | Why test it | +|---|---|---|---|---|---|---| +| 1 | `DELETE /users/v1/{username}` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **8** | write endpoint with no visible guard; IDOR/BOLA candidate (object id in path); enumerable path param on an unguarded route | +| 2 | `PUT /users/v1/{username}/email` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **8** | write endpoint with no visible guard; IDOR/BOLA candidate (object id in path); enumerable path param on an unguarded route | +| 3 | `PUT /users/v1/{username}/password` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **8** | write endpoint with no visible guard; IDOR/BOLA candidate (object id in path); enumerable path param on an unguarded route | +| 4 | `GET /books/v1/{book_title}` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **7** | no visible auth guard; IDOR/BOLA candidate (object id in path); enumerable path param on an unguarded route | +| 5 | `GET /users/v1/{username}` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **7** | no visible auth guard; IDOR/BOLA candidate (object id in path); enumerable path param on an unguarded route | +| 6 | `GET /` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **6** | no visible auth guard; IDOR/BOLA candidate (object id in path) | +| 7 | `GET /books/v1` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **6** | no visible auth guard; IDOR/BOLA candidate (object id in path) | +| 8 | `GET /users/v1` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **6** | no visible auth guard; IDOR/BOLA candidate (object id in path) | +| 9 | `POST /books/v1` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **4** | write endpoint with no visible guard | +| 10 | `GET /createdb` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **3** | no visible auth guard | +| 11 | `GET /me` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **3** | no visible auth guard | +| 12 | `GET /users/v1/_debug` | UNGUARDED | `openapi_specs/openapi3.yml` | — | **3** | no visible auth guard | +| 13 | `POST /users/v1/login` | public (intentional) | `openapi_specs/openapi3.yml` | — | **1** | auth endpoint (credential-attack surface) | +| 14 | `POST /users/v1/register` | public (intentional) | `openapi_specs/openapi3.yml` | — | **0** | — | + +## 3e. ★ API contract — shadow endpoints & spec hygiene + +Found **1 spec(s)**. **0 undocumented endpoint(s)** in code · 0 documented-but-absent. + +_⚠ A YAML spec was parsed with a bounded regex (websec ships no YAML dependency), so its operation-level results are PARTIAL — paths are reliable, per-operation `security` is not checked for YAML._ + +**Contract hygiene:** + +- `openapi3.yml`: plaintext server URL(s): http://localhost:5000 + +## 3b. ★ Access control (who can reach what — your #1 test) + +Guard coverage (file-level heuristic): 0 with visible guard · 14 none visible · 0 unknown. Global auth middleware: **False**. Roles in code: _none detected_ + +No global/mount auth middleware detected. Write endpoints with no visible guard are high-signal missing-authz leads — verify each. + +**Write endpoints with NO guard visible in their handler file (verify)** (4): +- DELETE /users/v1/{username} (openapi_specs/openapi3.yml) +- POST /books/v1 (openapi_specs/openapi3.yml) +- PUT /users/v1/{username}/email (openapi_specs/openapi3.yml) +- PUT /users/v1/{username}/password (openapi_specs/openapi3.yml) + +_No Next.js middleware.ts found — auth is per-handler._ + +## 3c. Config, CI/CD & client-side risks + +**Pipeline / IaC** (3 finding(s)): +- **MEDIUM** `gha-unpinned-action` — `.github/workflows/docker-image.yml` — actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@v3, docker/login-action@v2, docker/setup-buildx-action@v2, docker/setup-qemu-action@v2 +- **MEDIUM** `docker-root` — `Dockerfile` — container runs as root (add a non-root USER) +- **LOW** `docker-no-healthcheck` — `Dockerfile` — no HEALTHCHECK defined + +**Client-side secret exposure** (ships to the browser if real): _none detected_ +Production source maps exposed: False + +**GraphQL surface:** _no GraphQL detected_ + +**Password policy (cross-route consistency):** _no password validators detected_ + +**Client integrity — man-in-the-browser / tamperable display:** +_no fund-redirecting display values detected (MITB class N/A)_ + +**WebSocket auth model (CSWSH determinant — is it an ambient cookie?):** no websocket detected + +**Cookie hardening (report-the-pass / gap):** _no Set-Cookie detected_ + +**File-upload security (#2b — sniff bytes, derive stored name, nosniff on serve):** +_no upload handlers detected_ + +**PII output boundary (#8 — verify by VALUE SHAPE, not field name):** +_no obvious raw-PII responses / dead masking controls_ + +**Third-party integrations:** none detected +_0 webhook endpoint(s); signature code present or none found_ + +**License / entitlement verification (revocation + seat-cap trust):** _no license/entitlement verification-trust gaps detected_ + +**Browser-extension client-trust (chrome.storage entitlement gate / host perms / world:MAIN):** +_no WebExtension manifest detected_ + +**LLM / AI-agent surface (OWASP LLM Top 10 — prompt injection, insecure output, excessive agency, unbounded):** +_no direct LLM SDK call sites detected_ + + +## 4. Static findings (no running app needed) + +Scanners available: none on PATH + +> ⚠️ The count below is **raw scanner output (pre-triage)** — expect mostly noise (vulnerable-looking +> patterns that are guarded, intended-public, or not exploitable). The **triaged, calibrated view** is the +> findings ledger in `REPORT.md` / `findings-ledger.json` — each finding there carries a `P(real)`. Start +> from the ledger and debate-verify; don't report these raw counts as vulnerabilities. + +_Detected but not executed — run `websec run --scan`._ + +Install for fuller coverage: +- **Trivy** (sca) — `brew install trivy # pin by digest in CI` +- **Gitleaks** (secrets) — `brew install gitleaks` +- **Semgrep/OpenGrep** (sast) — `pipx install semgrep # or opengrep for fully-OSS` +- **Checkov** (iac) — `pipx install checkov` +- **Bandit** (sast) — `pipx install bandit` +- **OSV-Scanner** (sca) — `brew install osv-scanner` +- **TruffleHog (live verification)** (secrets) — `brew install trufflehog # opt-in: websec run … --verify-secrets` +- **Prowler** (cloud) — `pipx install prowler # needs AWS creds` + +## 4b. ★★ What a DAST / pentest will report — predicted before you run one + +**A scan of the deployed app should raise ~1 alert class(es) that are already visible in your source. Fix these and that scan comes back clean on them:** + +| Scanner | Alert (id) | Comes from | Why it will fire | +|---|---|---|---| +| ZAP (passive) | Strict-Transport-Security Header Not Set (10035) | `(response headers)` | HTTPS responses without (complete) HSTS | + +**⚠ A scanner will NOT find these — do not read a clean scan as "safe":** + +- **missing-auth** — a crawler sees a 200; it cannot know the route was SUPPOSED to require auth. + _seen at:_ `/books/v1`, `/users/v1/{username}` + +_These are what the staged probes in §5 are for: they need identities, state, and intent that no crawler has._ + +## 4c. Pre-triage — what a reviewer would filter (tagged, NOT dropped) + +_No findings match the standard reviewer-exclusion categories — the ledger is already high-signal._ + +## 5. Tailored probes (staged — drafts you finalize against §2–§3) + +- **unauth-baseline** — Missing authentication (no-creds baseline) + `probes/unauth-baseline.sh` · _supply:_ just the target base URL — it reads the routes from probe-context.json +- **rate-limit-burst** — Rate-limit + X-Forwarded-For bypass + `probes/rate-limit-burst.sh` · _supply:_ the login + a rate-limited endpoint +- **forged-token** — Forged/unsigned-JWT acceptance (CWE-347 broken auth) + `probes/forged-token.sh` · _supply:_ just the target base URL — it forges its own token + reads routes from probe-context.json +- **jwt-attacks** — JWT: alg:none, tamper, expiry, replay + `probes/jwt-attacks.sh` · _supply:_ a valid token + the login + a protected endpoint +- **hs256-brute-force** — Offline HS256 weak-secret brute + `probes/hs256-brute-force.py` · _supply:_ one HS256 JWT (offline — no live app needed) +- **mass-assignment** — BOPLA / mass assignment (OWASP API #3) + `probes/mass-assignment.py` · _supply:_ a low-priv token + a write endpoint that updates a record +- **webhook-forgery** — Inbound webhook signature/replay + `probes/webhook-forgery.py` · _supply:_ the webhook path + signature header name + scheme +- **race-conditions** — Race / claim-collision invariants + `probes/race-conditions.py` · _supply:_ a token + an endpoint with a single-winner invariant + an idempotency key +- **client-integrity-checklist** — Man-in-the-browser / tamperable-display posture + `probes/client-integrity-checklist.sh` · _supply:_ a running TEST instance + the page that shows the address (PAGE=/receive) + +Keep these in the repo after you run them — re-running after a fix proves "still blocked, now safer." + +## 5b. ★★ Pentest runbook — phased & pre-aimed (opt-in, against an instance you own) + +_A phased runbook aimed at THIS app's surface — run top-to-bottom against an instance you own (`$BASE_URL`). websec stages these; it never runs them. Each item carries a confirm/disconfirm oracle so you know what a real hit looks like._ + +### Phase 1 — SAFE recon (no auth, non-destructive) — run first, always +_Gate: none — read-only._ + +1. **TLS / cipher configuration** — `$BASE_URL (host)` + tool: testssl.sh + ``` + testssl.sh --quiet --color 0 $BASE_URL + ``` + _oracle:_ any 'NOT ok' line for protocol/cipher/cert = a finding; clean = pass + +2. **Security response headers** — `$BASE_URL (any page)` + tool: nuclei + ``` + nuclei -u $BASE_URL -tags headers,misconfig -severity low,medium,high + ``` + _oracle:_ confirms the §4b header predictions (CSP/HSTS/nosniff/frame-options) + +3. **Exposed VCS / dotfiles under web root** — `$BASE_URL/.git/config` + tool: nuclei + ``` + nuclei -u $BASE_URL -tags exposure,config + ``` + _oracle:_ a 200 with real content = exposed; 404/403 = pass + +### Phase 2 — AUTHZ (needs ≥2 identities you control) — the highest-value, scanner-blind class +_Gate: needs test accounts at ≥2 privilege levels._ + +1. **BOLA / IDOR — object id in path (`username`)** — `DELETE /users/v1/{username}` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/users/v1/{username}" # with A's username + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +2. **BOLA / IDOR — object id in path (`username`)** — `PUT /users/v1/{username}/email` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/users/v1/{username}/email" # with A's username + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +3. **BOLA / IDOR — object id in path (`username`)** — `PUT /users/v1/{username}/password` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/users/v1/{username}/password" # with A's username + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +4. **BOLA / IDOR — object id in path (`book_title`)** — `GET /books/v1/{book_title}` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/books/v1/{book_title}" # with A's book_title + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +5. **BOLA / IDOR — object id in path (`username`)** — `GET /users/v1/{username}` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/users/v1/{username}" # with A's username + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +6. **BOLA / IDOR — object id in path (`id`)** — `GET /` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/" # with A's id + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +7. **BOLA / IDOR — object id in path (`id`)** — `GET /books/v1` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/books/v1" # with A's id + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +8. **BOLA / IDOR — object id in path (`id`)** — `GET /users/v1` + tool: two identities (curl/httpie) or the staged bola probe + ``` + # as user A, note an id you own; then as user B: +curl -s -H "Authorization: Bearer $TOKEN_B" "$BASE_URL/users/v1" # with A's id + ``` + _oracle:_ user B gets user A's object (200 + A's data) = BOLA; 403/404 = properly isolated + +9. **BFLA / missing function-level authz — write with no visible guard** — `DELETE /users/v1/{username}` + tool: curl (no token, then low-priv token) + ``` + curl -s -X DELETE "$BASE_URL/users/v1/{username}" -H "Content-Type: application/json" -d '{}' + ``` + _oracle:_ a 2xx without auth (or from a low-priv user) = broken function-level authz + +10. **BFLA / missing function-level authz — write with no visible guard** — `PUT /users/v1/{username}/email` + tool: curl (no token, then low-priv token) + ``` + curl -s -X PUT "$BASE_URL/users/v1/{username}/email" -H "Content-Type: application/json" -d '{}' + ``` + _oracle:_ a 2xx without auth (or from a low-priv user) = broken function-level authz + +11. **BFLA / missing function-level authz — write with no visible guard** — `PUT /users/v1/{username}/password` + tool: curl (no token, then low-priv token) + ``` + curl -s -X PUT "$BASE_URL/users/v1/{username}/password" -H "Content-Type: application/json" -d '{}' + ``` + _oracle:_ a 2xx without auth (or from a low-priv user) = broken function-level authz + +12. **BFLA / missing function-level authz — write with no visible guard** — `POST /books/v1` + tool: curl (no token, then low-priv token) + ``` + curl -s -X POST "$BASE_URL/books/v1" -H "Content-Type: application/json" -d '{}' + ``` + _oracle:_ a 2xx without auth (or from a low-priv user) = broken function-level authz + + +## 5c. ★ Fix prompts — paste one straight into your agent + +_One self-contained instruction per finding — paste a block straight into your coding agent. Each ends with a VERIFY step, so "fixed" means demonstrated, not asserted._ + +
+1. [HIGH] missing-auth/books/v1 + +```text +Fix a `missing-auth` issue in `/books/v1`. + +What websec found: Missing authorization: POST /books/v1 +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+2. [HIGH] missing-auth/users/v1/{username} + +```text +Fix a `missing-auth` issue in `/users/v1/{username}`. + +What websec found: Missing authorization: DELETE /users/v1/{username} +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+3. [HIGH] missing-auth/users/v1/{username}/email + +```text +Fix a `missing-auth` issue in `/users/v1/{username}/email`. + +What websec found: Missing authorization: PUT /users/v1/{username}/email +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+4. [HIGH] missing-auth/users/v1/{username}/password + +```text +Fix a `missing-auth` issue in `/users/v1/{username}/password`. + +What websec found: Missing authorization: PUT /users/v1/{username}/password +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+5. [MEDIUM] missing-auth/ + +```text +Fix a `missing-auth` issue in `/`. + +What websec found: Missing authorization: GET / +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+6. [MEDIUM] missing-auth/books/v1 + +```text +Fix a `missing-auth` issue in `/books/v1`. + +What websec found: Missing authorization: GET /books/v1 +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+7. [MEDIUM] missing-auth/books/v1/{book_title} + +```text +Fix a `missing-auth` issue in `/books/v1/{book_title}`. + +What websec found: Missing authorization: GET /books/v1/{book_title} +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+8. [MEDIUM] missing-auth/createdb + +```text +Fix a `missing-auth` issue in `/createdb`. + +What websec found: Missing authorization: GET /createdb +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+9. [MEDIUM] missing-auth/me + +```text +Fix a `missing-auth` issue in `/me`. + +What websec found: Missing authorization: GET /me +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+10. [MEDIUM] missing-auth/users/v1 + +```text +Fix a `missing-auth` issue in `/users/v1`. + +What websec found: Missing authorization: GET /users/v1 +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+11. [MEDIUM] missing-auth/users/v1/{username} + +```text +Fix a `missing-auth` issue in `/users/v1/{username}`. + +What websec found: Missing authorization: GET /users/v1/{username} +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ +
+12. [MEDIUM] missing-auth/users/v1/_debug + +```text +Fix a `missing-auth` issue in `/users/v1/_debug`. + +What websec found: Missing authorization: GET /users/v1/_debug +Evidence: no auth guard found in handler openapi_specs/openapi3.yml +Standard: CWE-862 Missing Authorization +Recommended remediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. + +Calibrated prior: P(real)≈0.659 (n=41, class+label) — treat as a lead to verify, not a fact. + +Before changing anything: read the surrounding code and confirm this is genuinely exploitable in THIS codebase — websec reports leads, and a guarded or unreachable path is a false positive worth saying so about rather than 'fixing'. +After fixing, VERIFY: call the endpoint with no token and with a low-privilege token — both must be rejected. +``` +
+ + +## 6. How to work this — verify with a debate, then fix + +The findings ledger (`findings-ledger.json` / REPORT.md) comes pre-ranked with a **confidence** +(HIGH = dynamically confirmed; MEDIUM/LOW = hypothesis). Each finding also carries a **calibrated** +estimate — `calibrated.p` (measured real-vuln rate for that attack-class/confidence bucket on a +labeled vuln corpus), `calibrated.ci` (95% interval), `calibrated.n` (sample size), `calibrated.basis`. +**A wide CI or `basis: prior (uncalibrated)` means thin data — lean on the debate, not the number.** +The rates skew optimistic (the corpus is deliberately vulnerable); to be conservative, threshold on the +CI lower bound. **The calibration self-improves:** every `websec dynamic` run folds its *confirmed* +results (a write that executed unauthenticated = real; one that's auth-enforced = a recon false positive) +into a local overlay, so these numbers personalize to your apps the more you run it. **Verify before you +report** — especially MEDIUM/LOW — by running a 4-role debate per finding (this is the FP killer): + +- **Advocate** — argue it's real; cite the evidence chain + the CWE / OWASP-API. +- **Challenger** — try hard to *refute* it: false positive? intended-public? unreachable? guarded by a + pattern the static scan missed? (default to skepticism) +- **Mediator** — decide: confirmed / false-positive / needs-data. You may override the tool. +- **Explainer** — write the survivor up: exact `curl` repro, real impact, and the fix. + +**Generate probes the same way** — a Positive perspective (intended behavior holds) + Negative +(bypass / injection / error) + Edge (boundary / concurrency / unusual input), then a Critic dedupes +them into one runnable suite. More perspectives = broader coverage. + +**Verify the constitution** (`CONSTITUTION.md`): every ⬜ line is a Given/When/Then to confirm with a +probe — flip it to ✅ holds or 🔴 VIOLATED. + +Order: static triage (on a sql datastore, injection alerts are usually FPs) → +confirm the auth/tenant model → run §3-targeted probes (low-priv, then cross-tenant; record PASS counts +like "14/14 blocked") → fix what fails → re-run to confirm. **Human reviews every diff; never run +destructive or production probes without explicit authorization.** + +## 7. Hand back + +What was tested, what held (PASS counts), what's open (repro + fix), which probes are now regression tests. Cite `FACTS.json` + `scanners/`. + +--- + +## Appendix A — full endpoint inventory + +- `GET ` / +- `GET ` /books/v1 +- `POST ` /books/v1 +- `GET ` /books/v1/{book_title} +- `GET ` /createdb +- `GET ` /me +- `GET ` /users/v1 +- `DELETE` /users/v1/{username} +- `GET ` /users/v1/{username} +- `PUT ` /users/v1/{username}/email +- `PUT ` /users/v1/{username}/password +- `GET ` /users/v1/_debug +- `POST ` /users/v1/login +- `POST ` /users/v1/register diff --git a/vampi-out-patched/runs/20260727-031921/CONSTITUTION.md b/vampi-out-patched/runs/20260727-031921/CONSTITUTION.md new file mode 100644 index 0000000..d470b0a --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/CONSTITUTION.md @@ -0,0 +1,22 @@ +# Security constitution + +> Invariants this app must uphold, derived from recon. The dynamic probes verify them; a dynamically-confirmed finding flips one to 🔴 VIOLATED. Treat ⬜ as a hypothesis to confirm. + +**13 invariants · 0 VIOLATED · 13 to verify** + +## Authentication +- ⬜ verify — Given no auth token, When `GET /`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /books/v1`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `POST /books/v1`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /books/v1/{book_title}`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /createdb`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /me`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /users/v1`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `DELETE /users/v1/{username}`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /users/v1/{username}`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `PUT /users/v1/{username}/email`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `PUT /users/v1/{username}/password`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ +- ⬜ verify — Given no auth token, When `GET /users/v1/_debug`, Then 401/403 (no body, no mutation) · _openapi_specs/openapi3.yml_ + +## Secret hygiene +- ⬜ verify — Given the repo + git history, Then no live credential is present and no secret reaches the client bundle · _recon_ diff --git a/vampi-out-patched/runs/20260727-031921/FACTS.json b/vampi-out-patched/runs/20260727-031921/FACTS.json new file mode 100644 index 0000000..18d606e --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/FACTS.json @@ -0,0 +1,648 @@ +{ + "tool": "websec-validator", + "schema_version": "1.0", + "version": "0.11.0", + "target": "/home/jules/.cache/websec-corpus/VAmPI", + "files_scanned": 11, + "files_truncated": false, + "file_cap": 12000, + "stack": { + "languages": [ + "python" + ], + "frameworks": [ + "flask" + ], + "package_managers": [ + "pip" + ], + "datastores": [ + "sql-orm" + ], + "cron_triggers": [], + "monorepo": false, + "services": 0 + }, + "routes": { + "engine": "noir (openapi-first: own spec is the route contract)", + "count": 14, + "by_method": { + "GET": 8, + "POST": 3, + "DELETE": 1, + "PUT": 2 + }, + "by_technology": { + "oas3": 14 + }, + "endpoints": [ + { + "method": "GET", + "path": "/", + "params": [], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/books/v1", + "params": [], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "POST", + "path": "/books/v1", + "params": [ + { + "name": "book_title", + "where": "json" + }, + { + "name": "secret", + "where": "json" + }, + { + "name": "Authorization", + "where": "header" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/books/v1/{book_title}", + "params": [ + { + "name": "book_title", + "where": "path" + }, + { + "name": "Authorization", + "where": "header" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/createdb", + "params": [], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/me", + "params": [ + { + "name": "Authorization", + "where": "header" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/users/v1", + "params": [], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "DELETE", + "path": "/users/v1/{username}", + "params": [ + { + "name": "username", + "where": "path" + }, + { + "name": "Authorization", + "where": "header" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/users/v1/{username}", + "params": [ + { + "name": "username", + "where": "path" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "PUT", + "path": "/users/v1/{username}/email", + "params": [ + { + "name": "username", + "where": "path" + }, + { + "name": "email", + "where": "json" + }, + { + "name": "Authorization", + "where": "header" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "PUT", + "path": "/users/v1/{username}/password", + "params": [ + { + "name": "username", + "where": "path" + }, + { + "name": "password", + "where": "json" + }, + { + "name": "Authorization", + "where": "header" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "GET", + "path": "/users/v1/_debug", + "params": [], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "POST", + "path": "/users/v1/login", + "params": [ + { + "name": "username", + "where": "json" + }, + { + "name": "password", + "where": "json" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + }, + { + "method": "POST", + "path": "/users/v1/register", + "params": [ + { + "name": "username", + "where": "json" + }, + { + "name": "password", + "where": "json" + }, + { + "name": "email", + "where": "json" + } + ], + "technology": "oas3", + "code_path": "/home/jules/.cache/websec-corpus/VAmPI/openapi_specs/openapi3.yml", + "source": "noir" + } + ], + "targeting": { + "write_endpoints": [ + "DELETE /users/v1/{username}", + "POST /books/v1", + "POST /users/v1/login", + "POST /users/v1/register", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password" + ], + "idor_candidates": [ + "DELETE /users/v1/{username}", + "GET /books/v1/{book_title}", + "GET /users/v1/{username}", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password" + ], + "ssrf_candidates": [], + "open_redirect_candidates": [], + "upload_candidates": [], + "auth_endpoints": [ + "POST /users/v1/login" + ] + }, + "handler_signals": 0, + "coverage_warning": null, + "client_routes_excluded": 0, + "serverless_public_endpoints": 0, + "raw_server_endpoints": 0 + }, + "auth": { + "scheme": "jwt (bearer)", + "schemes_detected": [ + "jwt (bearer)" + ], + "token_location": "bearer", + "login_endpoints": [ + "POST /users/v1/login" + ], + "cookie_names": [], + "guard_files": [], + "signal_counts": { + "jwt": 2, + "passport": 0, + "session": 0, + "api_key": 0, + "hmac": 0 + }, + "insecure_secret_defaults": [], + "broken_auth": [], + "jwt_sign_verify_present": true, + "route_count": 14, + "reliable_signal": true, + "note": "AGENT: confirm the PRIMARY auth flow + how a test token is minted before the JWT/auth probes. Multiple schemes often mean primary bearer/session + secondary SSO." + }, + "authz": { + "global_auth_middleware": false, + "mount_auth_detected": false, + "mount_authed_factories": [], + "mount_covered_files": 0, + "next_middleware": { + "present": false, + "matchers": [], + "is_auth": false + }, + "roles_detected": [], + "guard_summary": { + "with_visible_guard": 0, + "no_visible_guard": 14, + "unknown": 0 + }, + "endpoint_guards": [ + { + "method": "GET", + "path": "/", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/books/v1", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "POST", + "path": "/books/v1", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/books/v1/{book_title}", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/createdb", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/me", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/users/v1", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "DELETE", + "path": "/users/v1/{username}", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/users/v1/{username}", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "PUT", + "path": "/users/v1/{username}/email", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "PUT", + "path": "/users/v1/{username}/password", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "GET", + "path": "/users/v1/_debug", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": false + }, + { + "method": "POST", + "path": "/users/v1/login", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": true + }, + { + "method": "POST", + "path": "/users/v1/register", + "code_path": "openapi_specs/openapi3.yml", + "guarded": false, + "analyzed": true, + "public_hint": true + } + ], + "endpoint_guards_truncated": 0, + "write_endpoints_without_visible_guard": [ + "DELETE /users/v1/{username} (openapi_specs/openapi3.yml)", + "POST /books/v1 (openapi_specs/openapi3.yml)", + "PUT /users/v1/{username}/email (openapi_specs/openapi3.yml)", + "PUT /users/v1/{username}/password (openapi_specs/openapi3.yml)" + ], + "unsafe_auth_decoders": [], + "unverified_signature_routes": [], + "note": "No global/mount auth middleware detected. Write endpoints with no visible guard are high-signal missing-authz leads \u2014 verify each." + }, + "tenant": { + "candidates": [], + "multi_tenant_likely": false, + "route_count": 14, + "note": "AGENT: confirm with the human which key (if any) is THE tenant boundary. If single-tenant, skip the cross-tenant BOLA probes." + }, + "password_policy": { + "password_blocks": [], + "strongest_policy": [], + "drift": [], + "weak_policy": null, + "password_reuse": { + "hashes_passwords": false, + "has_set_password_path": false, + "has_reuse_check": false, + "model_has_passwordHash": false, + "model_has_history": false, + "gap": false + }, + "consistent": true, + "note": "Heuristic over Zod/Joi/express-validator lookahead-required classes \u2014 verify against the actual validators; a helper-centralized policy can hide here." + }, + "surface": { + "sinks": {}, + "sink_counts": {}, + "ssrf_redirect_unguarded": [], + "proxy_prefix_escape": [], + "host_header_redirect": [], + "follows_redirect_no_allowlist": [], + "datastore_class": "sql", + "note": "Each sink hit is user-input-gated (req./request./concat/interp), so these are higher-confidence leads. Cross-reference the files with routes.targeting to pick the endpoint to probe. On a NoSQL/JSON API, SQLi alerts from generic scanners are usually false positives." + }, + "upload_security": { + "findings": [], + "upload_handlers": [], + "serve_paths_no_nosniff": [], + "by_severity": {}, + "note": "No upload handlers detected. Probe with the upload matrix (polyglot, spoofed MIME, double-extension, SVG) then FETCH the stored object back and assert it's served as octet-stream/attachment with nosniff." + }, + "schemas": { + "orms": [], + "entity_count": 0, + "entities": [], + "sensitive_fields": [], + "sql_ddl_present": false, + "sql_table_count": 0, + "owner_scoped_tables": [], + "rls_policy_count": 0, + "rls_enabled_count": 0, + "note": "Mass-assignment/BOPLA probes should try injecting these app-specific privileged fields into update/create payloads; ownership/tenant fields here are what BOLA must isolate." + }, + "iac_ci": { + "findings": [ + { + "severity": "MEDIUM", + "kind": "gha-unpinned-action", + "file": ".github/workflows/docker-image.yml", + "detail": "actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@v3, docker/login-action@v2, docker/setup-buildx-action@v2, docker/setup-qemu-action@v2" + }, + { + "severity": "MEDIUM", + "kind": "docker-root", + "file": "Dockerfile", + "detail": "container runs as root (add a non-root USER)" + }, + { + "severity": "LOW", + "kind": "docker-no-healthcheck", + "file": "Dockerfile", + "detail": "no HEALTHCHECK defined" + } + ], + "by_severity": { + "MEDIUM": 2, + "LOW": 1 + }, + "workflows_scanned": 1 + }, + "client_exposure": { + "public_env_vars": [], + "public_secret_leaks": [], + "intended_public_analytics": [], + "server_secret_in_client_component": [], + "public_secret_value_leaks": [], + "public_var_from_cfn_output": [], + "intended_public_supabase": [], + "supabase_service_role_in_client": [], + "production_source_maps": false, + "note": "public_secret_leaks / server_secret_in_client_component / public_secret_value_leaks / public_var_from_cfn_output ship secrets to the browser \u2014 treat as HIGH and confirm. Name-based leaks are gated to packages with a frontend bundler (a backend service's NEXT_PUBLIC_*/PUBLIC_* fallback-key reference is not a browser leak); analytics ingest tokens (PostHog/Usertour/\u2026) are reported separately at INFO (designed to ship). Value/CFN-injection detection survives a benign var rename (the #3 gap)." + }, + "client_integrity": { + "sensitive_display": [], + "sink_blast_radius": {}, + "websocket_auth": "no websocket detected", + "qr_generation": [], + "clipboard_copy": [], + "strict_csp": false, + "csp_present": false, + "csp_has_unsafe": false, + "out_of_band_anchor": false, + "anchors_found": [], + "weak_fingerprints": [], + "overclaimed_controls": [], + "client_fetch_sinks": [], + "findings": [], + "note": "No security-critical display values detected \u2014 MITB/tamperable-display class N/A." + }, + "transport_security": { + "web_surface": true, + "html_surface": false, + "csp_present": false, + "strict_csp": false, + "csp_has_unsafe": false, + "hsts_present": false, + "hsts_includes_subdomains": false, + "hsts_preload": false, + "hsts_files": [], + "clickjacking_protected": false, + "csrf_plumbing_present": false, + "inline_event_handlers": [], + "cookie_security": null, + "passes": [], + "findings": [ + { + "severity": "LOW", + "kind": "no-hsts", + "attack_class": "incomplete-hsts", + "detail": "No Strict-Transport-Security header found. Apply HSTS at the edge to ALL responses (`max-age>=31536000; includeSubDomains; preload` where the domain model allows) so the first-load page can't be downgraded over plaintext." + } + ], + "note": "CSP/HSTS baseline audit \u2014 these are the enabling controls for the client trust boundary. LOW/architectural: verify against the LIVE response headers (a static scan can't see the edge/CDN layer)." + }, + "pii_exposure": { + "findings": [], + "dead_controls": [], + "raw_pii_responses": [], + "masking_helpers": [], + "by_severity": {}, + "note": "PII output-boundary review: no obvious raw-PII responses. Probe with a per-role response diff asserting NO phone/email VALUE (/\\+?\\d{7,}/ or an email regex) reaches a non-privileged caller \u2014 across nested objects, IDs, and exports (#8)." + }, + "graphql": { + "present": false + }, + "integrations": { + "webhook_endpoints": [], + "webhooks_without_sig_verification": [], + "third_party_integrations": [], + "findings": [], + "note": "Webhooks with no signature-verification code in their handler = forgery/replay risk (run webhook-forgery; verify against your middleware). Each integration is an outbound trust + secret-handling surface (SSRF, secret leakage, supply-chain)." + }, + "llm_security": { + "is_ai_app": false, + "llm_call_sites": [], + "findings": [], + "by_severity": {}, + "note": "No direct LLM SDK call sites detected. LLM findings are leads (regex, not dataflow) \u2014 verify each: is the content untrusted, does model output drive a tool, is the action human-gated, is generation bounded? The agentic surface (prompt construction \u2192 tool dispatch \u2192 output) is where the real risk in an AI app lives." + }, + "crypto_usage": { + "findings": [], + "by_severity": {}, + "note": "No weak-hash / unpinned-verify / predictable-principal crypto tells found." + }, + "authz_dataflow": { + "findings": [], + "by_severity": {}, + "note": "No unsigned-cookie / claim-keyed-authz / transaction-local-RLS tells found." + }, + "webext": { + "is_extension": false, + "manifest_version": null, + "permissions": [], + "host_permissions": [], + "main_world_content_script": false, + "client_entitlement_gates": [], + "findings": [], + "note": "No WebExtension manifest detected \u2014 extension client-trust class N/A." + }, + "agent_config": { + "agent_surface_present": false, + "files_scanned": [], + "mcp_servers": [], + "findings": [], + "by_severity": {}, + "note": "Repo-own agent/MCP config read as untrusted data (never executed). Tool-description poisoning is intentionally not scanned here (deferred \u2014 prose-grammar, higher FP)." + }, + "dependencies": { + "manifests_scanned": 1, + "ecosystems": [ + "pip" + ], + "lockfiles_present": [], + "findings": [], + "unpinned": [ + { + "file": "requirements.txt", + "name": "swagger-ui-bundle", + "spec": "*", + "reason": "unpinned pip requirement (no == pin)" + } + ], + "confusion_candidates": [], + "counts": { + "unpinned": 1, + "confusion_candidates": 0, + "install_scripts": 0, + "drift": 0 + }, + "network": { + "ran": false, + "note": "registry resolution / known-hallucinated-name list / typosquat distance are deferred behind an opt-in --network step; the default pass is fully offline." + } + } +} \ No newline at end of file diff --git a/vampi-out-patched/runs/20260727-031921/REPORT.md b/vampi-out-patched/runs/20260727-031921/REPORT.md new file mode 100644 index 0000000..e80ad83 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/REPORT.md @@ -0,0 +1,138 @@ +# websec-validator report — /home/jules/.cache/websec-corpus/VAmPI + +> Generated 20260727-031921 · websec-validator v0.11.0 · **immutable run record** (never overwritten). +> Deterministic recon — no LLM. Hand `AGENT-BRIEFING.md` (same dir) to your coding agent to act on this. + +## Executive summary + +| | | +|---|---| +| Stack | python · flask · sql-orm | +| Endpoints | **14** app routes (via noir) | +| Auth | jwt (bearer) · roles: none | +| Access control | 0 guarded · **14 no visible guard** · global-middleware: False | +| Static scanner (raw, pre-triage) | _run with --scan for static findings_ | +| **Findings ledger** (triaged + calibrated) | **16 findings** · {'HIGH': 4, 'MEDIUM': 10, 'LOW': 2} · confidence {'MEDIUM': 15, 'LOW': 1} | +| Attack surface | IDOR: 5 · SSRF: 0 · upload: 0 · writes: 6 | + +## 1. Findings ledger (ranked · evidence chain · standards · confidence) + +- **[HIGH/MEDIUM]** Missing authorization: POST /books/v1 + `/books/v1` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[HIGH/MEDIUM]** Missing authorization: DELETE /users/v1/{username} + `/users/v1/{username}` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[HIGH/MEDIUM]** Missing authorization: PUT /users/v1/{username}/email + `/users/v1/{username}/email` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[HIGH/MEDIUM]** Missing authorization: PUT /users/v1/{username}/password + `/users/v1/{username}/password` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET / + `/` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /books/v1 + `/books/v1` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /books/v1/{book_title} + `/books/v1/{book_title}` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /createdb + `/createdb` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /me + `/me` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /users/v1 + `/users/v1` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /users/v1/{username} + `/users/v1/{username}` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** Missing authorization: GET /users/v1/_debug + `/users/v1/_debug` · evidence: recon · CWE-862 Missing Authorization · API1:2023 BOLA, API5:2023 BFLA · P(real)≈**0.659** CI [0.505, 0.784] (n=41, class+label) + _fix:_ Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten. +- **[MEDIUM/MEDIUM]** gha-unpinned-action: actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@ + `.github/workflows/docker-image.yml` · evidence: recon · CWE-1188 Insecure Default · P(real)≈**0.569** CI [0.433, 0.695] (n=51, label) + _fix:_ Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.). +- **[MEDIUM/MEDIUM]** docker-root: container runs as root (add a non-root USER) + `Dockerfile` · evidence: recon · CWE-1188 Insecure Default · P(real)≈**0.569** CI [0.433, 0.695] (n=51, label) + _fix:_ Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.). +- **[LOW/MEDIUM]** docker-no-healthcheck: no HEALTHCHECK defined + `Dockerfile` · evidence: recon · CWE-1188 Insecure Default · P(real)≈**0.569** CI [0.433, 0.695] (n=51, label) + _fix:_ Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.). +- **[LOW/LOW]** no-hsts: browser/transport hardening header + `(response headers)` · evidence: recon · CWE-523 Unprotected Transport of Credentials · API8:2023 Misconfiguration · P(real)≈**0.125** CI [0.022, 0.471] (n=8, label) + _fix:_ Apply HSTS uniformly at the EDGE to ALL responses (not just /api): `max-age>=31536000; includeSubDomains; preload` where the domain model allows. + +_Full ledger with complete evidence chains + remediation in `findings-ledger.json`. Confidence: HIGH = dynamically confirmed or verified; MEDIUM = concrete static evidence; LOW = single-source hypothesis to verify._ + + +_**P(real)** = measured real-vuln rate for that attack-class/confidence bucket, with a 95% confidence interval and sample size `n` (indicative — calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code). A wide CI or `basis: prior (uncalibrated)` means thin data — lean on the verification debate, not the number; to be conservative, threshold on the CI lower bound._ + +## 2. Access control + +**⚠ Write endpoints with no visible guard (verify — top missing-authz leads)** (4): +- DELETE /users/v1/{username} (openapi_specs/openapi3.yml) +- POST /books/v1 (openapi_specs/openapi3.yml) +- PUT /users/v1/{username}/email (openapi_specs/openapi3.yml) +- PUT /users/v1/{username}/password (openapi_specs/openapi3.yml) + +No global/mount auth middleware detected. Write endpoints with no visible guard are high-signal missing-authz leads — verify each. + +## 3. Attack surface & targeting + +**IDOR / BOLA candidates** (5): +- DELETE /users/v1/{username} +- GET /books/v1/{book_title} +- GET /users/v1/{username} +- PUT /users/v1/{username}/email +- PUT /users/v1/{username}/password + +**SSRF candidates** (0): +_(none)_ + +**File-upload candidates** (0): +_(none)_ + +**Code-level sinks (user-input-gated):** none + +**Mass-assignment targets (privileged model fields):** none detected · ORMs: ? + +## 4. Config / CI-CD / client-side + +**IaC/CI:** 3 finding(s) · **GraphQL:** False · **client-side secret exposure:** 0 + +## 5. Staged probes + +- `unauth-baseline` — Missing authentication (no-creds baseline) +- `rate-limit-burst` — Rate-limit + X-Forwarded-For bypass +- `forged-token` — Forged/unsigned-JWT acceptance (CWE-347 broken auth) +- `jwt-attacks` — JWT: alg:none, tamper, expiry, replay +- `hs256-brute-force` — Offline HS256 weak-secret brute +- `mass-assignment` — BOPLA / mass assignment (OWASP API #3) +- `webhook-forgery` — Inbound webhook signature/replay +- `race-conditions` — Race / claim-collision invariants +- `client-integrity-checklist` — Man-in-the-browser / tamperable-display posture + +## Appendix — endpoint inventory + +- `GET ` / +- `GET ` /books/v1 +- `POST ` /books/v1 +- `GET ` /books/v1/{book_title} +- `GET ` /createdb +- `GET ` /me +- `GET ` /users/v1 +- `DELETE` /users/v1/{username} +- `GET ` /users/v1/{username} +- `PUT ` /users/v1/{username}/email +- `PUT ` /users/v1/{username}/password +- `GET ` /users/v1/_debug +- `POST ` /users/v1/login +- `POST ` /users/v1/register + +--- +_Roadmap: this report grows into a traceable findings ledger — each finding gaining an evidence +chain (recon → static → dynamic), an OWASP/CWE citation, and a calibrated H/M/L confidence._ diff --git a/vampi-out-patched/runs/20260727-031921/attack-surface.json b/vampi-out-patched/runs/20260727-031921/attack-surface.json new file mode 100644 index 0000000..aec2f4d --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/attack-surface.json @@ -0,0 +1,272 @@ +{ + "endpoints": [ + { + "method": "DELETE", + "path": "/users/v1/{username}", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [ + "username" + ], + "params": [ + "username", + "Authorization" + ], + "sinks": [], + "risk": 8, + "why": [ + "write endpoint with no visible guard", + "IDOR/BOLA candidate (object id in path)", + "enumerable path param on an unguarded route" + ], + "is_write": true + }, + { + "method": "PUT", + "path": "/users/v1/{username}/email", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [ + "username" + ], + "params": [ + "username", + "email", + "Authorization" + ], + "sinks": [], + "risk": 8, + "why": [ + "write endpoint with no visible guard", + "IDOR/BOLA candidate (object id in path)", + "enumerable path param on an unguarded route" + ], + "is_write": true + }, + { + "method": "PUT", + "path": "/users/v1/{username}/password", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [ + "username" + ], + "params": [ + "username", + "password", + "Authorization" + ], + "sinks": [], + "risk": 8, + "why": [ + "write endpoint with no visible guard", + "IDOR/BOLA candidate (object id in path)", + "enumerable path param on an unguarded route" + ], + "is_write": true + }, + { + "method": "GET", + "path": "/books/v1/{book_title}", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [ + "book_title" + ], + "params": [ + "book_title", + "Authorization" + ], + "sinks": [], + "risk": 7, + "why": [ + "no visible auth guard", + "IDOR/BOLA candidate (object id in path)", + "enumerable path param on an unguarded route" + ], + "is_write": false + }, + { + "method": "GET", + "path": "/users/v1/{username}", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [ + "username" + ], + "params": [ + "username" + ], + "sinks": [], + "risk": 7, + "why": [ + "no visible auth guard", + "IDOR/BOLA candidate (object id in path)", + "enumerable path param on an unguarded route" + ], + "is_write": false + }, + { + "method": "GET", + "path": "/", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [], + "sinks": [], + "risk": 6, + "why": [ + "no visible auth guard", + "IDOR/BOLA candidate (object id in path)" + ], + "is_write": false + }, + { + "method": "GET", + "path": "/books/v1", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [], + "sinks": [], + "risk": 6, + "why": [ + "no visible auth guard", + "IDOR/BOLA candidate (object id in path)" + ], + "is_write": false + }, + { + "method": "GET", + "path": "/users/v1", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [], + "sinks": [], + "risk": 6, + "why": [ + "no visible auth guard", + "IDOR/BOLA candidate (object id in path)" + ], + "is_write": false + }, + { + "method": "POST", + "path": "/books/v1", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [ + "book_title", + "secret", + "Authorization" + ], + "sinks": [], + "risk": 4, + "why": [ + "write endpoint with no visible guard" + ], + "is_write": true + }, + { + "method": "GET", + "path": "/createdb", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [], + "sinks": [], + "risk": 3, + "why": [ + "no visible auth guard" + ], + "is_write": false + }, + { + "method": "GET", + "path": "/me", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [ + "Authorization" + ], + "sinks": [], + "risk": 3, + "why": [ + "no visible auth guard" + ], + "is_write": false + }, + { + "method": "GET", + "path": "/users/v1/_debug", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "UNGUARDED", + "path_params": [], + "params": [], + "sinks": [], + "risk": 3, + "why": [ + "no visible auth guard" + ], + "is_write": false + }, + { + "method": "POST", + "path": "/users/v1/login", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "public (intentional)", + "path_params": [], + "params": [ + "username", + "password" + ], + "sinks": [], + "risk": 1, + "why": [ + "auth endpoint (credential-attack surface)" + ], + "is_write": true + }, + { + "method": "POST", + "path": "/users/v1/register", + "handler": "openapi_specs/openapi3.yml", + "technology": "oas3", + "auth": "public (intentional)", + "path_params": [], + "params": [ + "username", + "password", + "email" + ], + "sinks": [], + "risk": 0, + "why": [], + "is_write": true + } + ], + "summary": { + "endpoints": 14, + "unguarded": 12, + "unguarded_writes": 4, + "with_high_risk_sink": 0, + "idor_candidates": 5, + "top_risk": 8 + } +} \ No newline at end of file diff --git a/vampi-out-patched/runs/20260727-031921/findings-ledger.json b/vampi-out-patched/runs/20260727-031921/findings-ledger.json new file mode 100644 index 0000000..381a524 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/findings-ledger.json @@ -0,0 +1,624 @@ +{ + "schema_version": "1.0", + "findings": [ + { + "title": "Missing authorization: POST /books/v1", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/books/v1", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "ccda9cf549971c16" + }, + { + "title": "Missing authorization: DELETE /users/v1/{username}", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/users/v1/{username}", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "c32968d10958c9c3" + }, + { + "title": "Missing authorization: PUT /users/v1/{username}/email", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/users/v1/{username}/email", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "f641e6b943ade7ec" + }, + { + "title": "Missing authorization: PUT /users/v1/{username}/password", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/users/v1/{username}/password", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "97e214fdac4029de" + }, + { + "title": "Missing authorization: GET /", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "52f7d8ce874b5b5d" + }, + { + "title": "Missing authorization: GET /books/v1", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/books/v1", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "c76a62be79e44ab2" + }, + { + "title": "Missing authorization: GET /books/v1/{book_title}", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/books/v1/{book_title}", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "82e88da3362d05d9" + }, + { + "title": "Missing authorization: GET /createdb", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/createdb", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "d39911c87724a075" + }, + { + "title": "Missing authorization: GET /me", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/me", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "b94f3c9ff43eae6d" + }, + { + "title": "Missing authorization: GET /users/v1", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/users/v1", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "920c877709b8d1e5" + }, + { + "title": "Missing authorization: GET /users/v1/{username}", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/users/v1/{username}", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "ab2771d8e049e06b" + }, + { + "title": "Missing authorization: GET /users/v1/_debug", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/users/v1/_debug", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "8f4ce95eb01b6b73" + }, + { + "title": "gha-unpinned-action: actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@", + "category": "iac-ci", + "attack_class": "iac", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": ".github/workflows/docker-image.yml", + "evidence": [ + { + "layer": "recon", + "detail": "actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@v3, docker/login-action@v2, docker/setup-buildx-action@v2, docker/setup-qemu-action@v2" + } + ], + "standards": { + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [] + }, + "remediation": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).", + "status": "open", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "89e7cb3176361d71" + }, + { + "title": "docker-root: container runs as root (add a non-root USER)", + "category": "iac-ci", + "attack_class": "iac", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "Dockerfile", + "evidence": [ + { + "layer": "recon", + "detail": "container runs as root (add a non-root USER)" + } + ], + "standards": { + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [] + }, + "remediation": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).", + "status": "open", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "dfc2c51eeac06ce9" + }, + { + "title": "docker-no-healthcheck: no HEALTHCHECK defined", + "category": "iac-ci", + "attack_class": "iac", + "severity": "LOW", + "confidence": "MEDIUM", + "location": "Dockerfile", + "evidence": [ + { + "layer": "recon", + "detail": "no HEALTHCHECK defined" + } + ], + "standards": { + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [] + }, + "remediation": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).", + "status": "open", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "fc886e46ce90c502" + }, + { + "title": "no-hsts: browser/transport hardening header", + "category": "transport", + "attack_class": "incomplete-hsts", + "severity": "LOW", + "confidence": "LOW", + "location": "(response headers)", + "evidence": [ + { + "layer": "recon", + "detail": "No Strict-Transport-Security header found. Apply HSTS at the edge to ALL responses (`max-age>=31536000; includeSubDomains; preload` where the domain model allows) so the first-load page can't be downgraded over plaintext." + } + ], + "standards": { + "cwe": [ + "CWE-523 Unprotected Transport of Credentials", + "CWE-319 Cleartext Transmission of Sensitive Information" + ], + "asvs": "ASVS V9.1.2", + "owasp_api": [ + "API8:2023 Misconfiguration" + ] + }, + "remediation": "Apply HSTS uniformly at the EDGE to ALL responses (not just /api): `max-age>=31536000; includeSubDomains; preload` where the domain model allows.", + "status": "open", + "calibrated": { + "p": 0.125, + "ci": [ + 0.022, + 0.471 + ], + "n": 8, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "339f3c8ca3588ced" + } + ], + "total": 16, + "suppressed": 0, + "acknowledged": [], + "acknowledged_n": 0, + "by_severity": { + "HIGH": 4, + "MEDIUM": 10, + "LOW": 2 + }, + "by_confidence": { + "MEDIUM": 15, + "LOW": 1 + }, + "calibration": { + "loaded": true, + "by_basis": { + "class+label": 12, + "label": 4 + }, + "personalized": false, + "local_samples": 0, + "caveat": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "dynamic_included": false +} \ No newline at end of file diff --git a/vampi-out-patched/runs/20260727-031921/findings.envelope.json b/vampi-out-patched/runs/20260727-031921/findings.envelope.json new file mode 100644 index 0000000..8c4c4c3 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/findings.envelope.json @@ -0,0 +1,629 @@ +{ + "schema_version": "1.0", + "tool": "websec-validator", + "tool_version": "0.11.0", + "generated": "20260727-031921", + "target": "/home/jules/.cache/websec-corpus/VAmPI", + "summary": { + "total": 16, + "by_severity": { + "HIGH": 4, + "MEDIUM": 10, + "LOW": 2 + }, + "by_confidence": { + "MEDIUM": 15, + "LOW": 1 + }, + "suppressed": 0, + "acknowledged": 0, + "dynamic_included": false + }, + "calibration": { + "loaded": true, + "by_basis": { + "class+label": 12, + "label": 4 + }, + "personalized": false, + "local_samples": 0, + "caveat": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "findings": [ + { + "title": "Missing authorization: POST /books/v1", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/books/v1", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "ccda9cf549971c16" + }, + { + "title": "Missing authorization: DELETE /users/v1/{username}", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/users/v1/{username}", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "c32968d10958c9c3" + }, + { + "title": "Missing authorization: PUT /users/v1/{username}/email", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/users/v1/{username}/email", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "f641e6b943ade7ec" + }, + { + "title": "Missing authorization: PUT /users/v1/{username}/password", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "HIGH", + "confidence": "MEDIUM", + "location": "/users/v1/{username}/password", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "97e214fdac4029de" + }, + { + "title": "Missing authorization: GET /", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "52f7d8ce874b5b5d" + }, + { + "title": "Missing authorization: GET /books/v1", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/books/v1", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "c76a62be79e44ab2" + }, + { + "title": "Missing authorization: GET /books/v1/{book_title}", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/books/v1/{book_title}", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "82e88da3362d05d9" + }, + { + "title": "Missing authorization: GET /createdb", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/createdb", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "d39911c87724a075" + }, + { + "title": "Missing authorization: GET /me", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/me", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "b94f3c9ff43eae6d" + }, + { + "title": "Missing authorization: GET /users/v1", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/users/v1", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "920c877709b8d1e5" + }, + { + "title": "Missing authorization: GET /users/v1/{username}", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/users/v1/{username}", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "ab2771d8e049e06b" + }, + { + "title": "Missing authorization: GET /users/v1/_debug", + "category": "access-control", + "attack_class": "missing-auth", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "/users/v1/_debug", + "evidence": [ + { + "layer": "recon", + "detail": "no auth guard found in handler openapi_specs/openapi3.yml" + } + ], + "standards": { + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ] + }, + "remediation": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.", + "status": "open", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "8f4ce95eb01b6b73" + }, + { + "title": "gha-unpinned-action: actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@", + "category": "iac-ci", + "attack_class": "iac", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": ".github/workflows/docker-image.yml", + "evidence": [ + { + "layer": "recon", + "detail": "actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@v3, docker/login-action@v2, docker/setup-buildx-action@v2, docker/setup-qemu-action@v2" + } + ], + "standards": { + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [] + }, + "remediation": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).", + "status": "open", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "89e7cb3176361d71" + }, + { + "title": "docker-root: container runs as root (add a non-root USER)", + "category": "iac-ci", + "attack_class": "iac", + "severity": "MEDIUM", + "confidence": "MEDIUM", + "location": "Dockerfile", + "evidence": [ + { + "layer": "recon", + "detail": "container runs as root (add a non-root USER)" + } + ], + "standards": { + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [] + }, + "remediation": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).", + "status": "open", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "dfc2c51eeac06ce9" + }, + { + "title": "docker-no-healthcheck: no HEALTHCHECK defined", + "category": "iac-ci", + "attack_class": "iac", + "severity": "LOW", + "confidence": "MEDIUM", + "location": "Dockerfile", + "evidence": [ + { + "layer": "recon", + "detail": "no HEALTHCHECK defined" + } + ], + "standards": { + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [] + }, + "remediation": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).", + "status": "open", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "fc886e46ce90c502" + }, + { + "title": "no-hsts: browser/transport hardening header", + "category": "transport", + "attack_class": "incomplete-hsts", + "severity": "LOW", + "confidence": "LOW", + "location": "(response headers)", + "evidence": [ + { + "layer": "recon", + "detail": "No Strict-Transport-Security header found. Apply HSTS at the edge to ALL responses (`max-age>=31536000; includeSubDomains; preload` where the domain model allows) so the first-load page can't be downgraded over plaintext." + } + ], + "standards": { + "cwe": [ + "CWE-523 Unprotected Transport of Credentials", + "CWE-319 Cleartext Transmission of Sensitive Information" + ], + "asvs": "ASVS V9.1.2", + "owasp_api": [ + "API8:2023 Misconfiguration" + ] + }, + "remediation": "Apply HSTS uniformly at the EDGE to ALL responses (not just /api): `max-age>=31536000; includeSubDomains; preload` where the domain model allows.", + "status": "open", + "calibrated": { + "p": 0.125, + "ci": [ + 0.022, + 0.471 + ], + "n": 8, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "fingerprint": "339f3c8ca3588ced" + } + ] +} \ No newline at end of file diff --git a/vampi-out-patched/runs/20260727-031921/manifest.json b/vampi-out-patched/runs/20260727-031921/manifest.json new file mode 100644 index 0000000..c1b7b61 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/manifest.json @@ -0,0 +1,169 @@ +{ + "facts": "FACTS.json", + "scanners": { + "available": [], + "missing": [ + { + "key": "trivy", + "name": "Trivy", + "category": "sca", + "install": "brew install trivy # pin by digest in CI" + }, + { + "key": "gitleaks", + "name": "Gitleaks", + "category": "secrets", + "install": "brew install gitleaks" + }, + { + "key": "semgrep", + "name": "Semgrep/OpenGrep", + "category": "sast", + "install": "pipx install semgrep # or opengrep for fully-OSS" + }, + { + "key": "checkov", + "name": "Checkov", + "category": "iac", + "install": "pipx install checkov" + }, + { + "key": "bandit", + "name": "Bandit", + "category": "sast", + "install": "pipx install bandit" + }, + { + "key": "osv-scanner", + "name": "OSV-Scanner", + "category": "sca", + "install": "brew install osv-scanner" + }, + { + "key": "trufflehog", + "name": "TruffleHog (live verification)", + "category": "secrets", + "install": "brew install trufflehog # opt-in: websec run \u2026 --verify-secrets" + }, + { + "key": "prowler", + "name": "Prowler", + "category": "cloud", + "install": "pipx install prowler # needs AWS creds" + } + ] + }, + "scan_results": [], + "findings_summary": null, + "ledger": { + "total": 16, + "by_severity": { + "HIGH": 4, + "MEDIUM": 10, + "LOW": 2 + } + }, + "sarif": "results.sarif", + "sbom": null, + "attack_surface": "attack-surface.json", + "attack_surface_summary": { + "endpoints": 14, + "unguarded": 12, + "unguarded_writes": 4, + "with_high_risk_sink": 0, + "idor_candidates": 5, + "top_risk": 8 + }, + "probes": [ + { + "key": "_context", + "file": "probes/probe-context.json", + "note": "the target's real routes/auth/fields \u2014 finalize the drafts against this" + }, + { + "key": "unauth-baseline", + "file": "probes/unauth-baseline.sh", + "attack_class": "Missing authentication (no-creds baseline)", + "agent_must_supply": "just the target base URL \u2014 it reads the routes from probe-context.json", + "targets": [ + "DELETE /users/v1/{username}", + "POST /books/v1", + "POST /users/v1/login", + "POST /users/v1/register", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password" + ] + }, + { + "key": "rate-limit-burst", + "file": "probes/rate-limit-burst.sh", + "attack_class": "Rate-limit + X-Forwarded-For bypass", + "agent_must_supply": "the login + a rate-limited endpoint", + "targets": [] + }, + { + "key": "forged-token", + "file": "probes/forged-token.sh", + "attack_class": "Forged/unsigned-JWT acceptance (CWE-347 broken auth)", + "agent_must_supply": "just the target base URL \u2014 it forges its own token + reads routes from probe-context.json", + "targets": [] + }, + { + "key": "jwt-attacks", + "file": "probes/jwt-attacks.sh", + "attack_class": "JWT: alg:none, tamper, expiry, replay", + "agent_must_supply": "a valid token + the login + a protected endpoint", + "targets": [] + }, + { + "key": "hs256-brute-force", + "file": "probes/hs256-brute-force.py", + "attack_class": "Offline HS256 weak-secret brute", + "agent_must_supply": "one HS256 JWT (offline \u2014 no live app needed)", + "targets": [] + }, + { + "key": "mass-assignment", + "file": "probes/mass-assignment.py", + "attack_class": "BOPLA / mass assignment (OWASP API #3)", + "agent_must_supply": "a low-priv token + a write endpoint that updates a record", + "targets": [ + "DELETE /users/v1/{username}", + "POST /books/v1", + "POST /users/v1/login", + "POST /users/v1/register", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password" + ] + }, + { + "key": "webhook-forgery", + "file": "probes/webhook-forgery.py", + "attack_class": "Inbound webhook signature/replay", + "agent_must_supply": "the webhook path + signature header name + scheme", + "targets": [ + "DELETE /users/v1/{username}", + "POST /books/v1", + "POST /users/v1/login", + "POST /users/v1/register", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password" + ] + }, + { + "key": "race-conditions", + "file": "probes/race-conditions.py", + "attack_class": "Race / claim-collision invariants", + "agent_must_supply": "a token + an endpoint with a single-winner invariant + an idempotency key", + "targets": [] + }, + { + "key": "client-integrity-checklist", + "file": "probes/client-integrity-checklist.sh", + "attack_class": "Man-in-the-browser / tamperable-display posture", + "agent_must_supply": "a running TEST instance + the page that shows the address (PAGE=/receive)", + "targets": [] + } + ], + "timestamp": "20260727-031921" +} \ No newline at end of file diff --git a/vampi-out-patched/runs/20260727-031921/probes/_lib.py b/vampi-out-patched/runs/20260727-031921/probes/_lib.py new file mode 100644 index 0000000..106007a --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/_lib.py @@ -0,0 +1,90 @@ +"""Shared probe helpers — load THIS target's real surface from probe-context.json +(written by `websec run`) and auth/ids from environment variables. + +Why env vars: recon gives you the real endpoints, auth scheme, and tenant key — but it +cannot mint live tokens or know real object ids. You (or your agent, against a TEST +instance) supply those: + + TARGET=http://localhost:3000 # base URL (or set target_base_url in probe-context.json) + TOKEN_A=... TOKEN_B=... # bearer JWTs for two test accounts (different tenants) + COOKIE_A=... COOKIE_B=... # OR session cookies (e.g. NextAuth) instead of bearer + APIKEY=... # OR an API key + OBJ_A=... OBJ_B=... # a sample object id owned by each account/tenant + GROUP_A=... GROUP_B=... # each account's tenant/group id (defaults to OBJ_* if unset) + +Run only against a TEST instance you're authorized to probe. Never production. +""" +import json +import os +import subprocess +import sys +from pathlib import Path + +_HERE = Path(__file__).resolve().parent + + +def context() -> dict: + p = _HERE / "probe-context.json" + if not p.is_file(): + sys.exit("probe-context.json not found next to this probe — run `websec run ` and use " + "the probes/ it stages (probe-context.json holds this app's real routes/auth).") + return json.loads(p.read_text()) + + +def base_url() -> str: + u = os.environ.get("TARGET") or context().get("target_base_url", "") + if not u or u.startswith("FILL"): + sys.exit("Set TARGET=http://host:port (or fill target_base_url in probe-context.json).") + return u.rstrip("/") + + +def auth_headers(role: str = "A") -> list: + """Auth header for a role (A/B), adapting to whatever the operator supplied.""" + tok = os.environ.get(f"TOKEN_{role}") + cookie = os.environ.get(f"COOKIE_{role}") + apikey = os.environ.get("APIKEY") + if tok: + return ["-H", f"Authorization: Bearer {tok}"] + if cookie: + return ["-H", f"Cookie: {cookie}"] + if apikey: + return ["-H", f"X-API-Key: {apikey}"] + return [] # unauthenticated + + +def require(*names: str) -> None: + missing = [n for n in names if not os.environ.get(n)] + if missing: + sys.exit(f"This probe needs these env var(s): {', '.join(missing)}. See _lib.py for the list.") + + +def curl(method: str, url: str, headers=None, body=None, timeout: int = 20): + """Returns (status_code, body_text). Never raises on HTTP errors.""" + cmd = ["curl", "-s", "-X", method, url, "-w", "\nHTTP_CODE:%{http_code}", + "--max-time", str(timeout)] + (headers or []) + if body is not None: + cmd += ["-H", "content-type: application/json", "-d", json.dumps(body)] + out = subprocess.run(cmd, capture_output=True, text=True).stdout + code = int(out.split("HTTP_CODE:")[-1].strip()) if "HTTP_CODE:" in out else 0 + return code, out.split("\nHTTP_CODE:")[0] + + +def tenant_key(default: str = "groupId") -> str: + keys = context().get("tenant_keys") or [] + return keys[0] if keys else default + + +def write_endpoints() -> list: + """[(METHOD, path), …] for this app's mutating routes, from probe-context.json.""" + out = [] + for ep in context().get("endpoints", {}).get("writes", []): + parts = ep.split(" ", 1) + if len(parts) == 2: + out.append((parts[0], parts[1])) + return out + + +def save(name: str, findings: list) -> Path: + out = _HERE / f"{name}-findings.json" + out.write_text(json.dumps(findings, indent=2) + "\n") + return out diff --git a/vampi-out-patched/runs/20260727-031921/probes/client-integrity-checklist.sh b/vampi-out-patched/runs/20260727-031921/probes/client-integrity-checklist.sh new file mode 100644 index 0000000..279b11f --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/client-integrity-checklist.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +# client-integrity-checklist.sh — man-in-the-browser posture for a page that displays a fund- +# redirecting value (wallet/receive address, QR, routing #). Auto-checks Layer A (strict CSP); prints +# the Layer B (out-of-band anchor) checklist for the human. Honest limit: on-screen display can't be +# made tamper-proof on the web — the goal is DETECTABLE tampering, not impossible. +set -uo pipefail +cd "$(dirname "$0")" +ctx=probe-context.json +TARGET="${TARGET:-$(python3 -c "import json;print(json.load(open('$ctx'))['target_base_url'])" 2>/dev/null)}" +PAGE="${PAGE:-/receive}" # the page that renders the address +if [ -z "${TARGET:-}" ] || [ "${TARGET#FILL}" != "$TARGET" ]; then echo "Set TARGET=http://host:port (PAGE=/receive)"; exit 2; fi + +hdrs=$(curl -s -m 8 -D - -o /dev/null "$TARGET$PAGE" 2>&1 || true) +csp=$(printf '%s' "$hdrs" | grep -i '^content-security-policy:' | head -1) +echo "== Layer A — strict CSP (kills the scalable supply-chain / injected-script vector) ==" +fails=0 +if [ -z "$csp" ]; then + echo " FAIL no Content-Security-Policy header on $PAGE"; fails=$((fails+1)) +else + printf ' CSP: %s\n' "$(printf '%s' "$csp" | head -c 240)" + printf '%s' "$csp" | grep -qiE "script-src[^;]*'self'" || { echo " FAIL script-src is not 'self'"; fails=$((fails+1)); } + printf '%s' "$csp" | grep -qiE "'nonce-|strict-dynamic" || echo " WARN no nonce / strict-dynamic in script-src" + if printf '%s' "$csp" | grep -qiE "'unsafe-(inline|eval)'"; then + echo " FAIL script-src allows unsafe-inline/eval — injected script still runs"; fails=$((fails+1)) + fi +fi +echo +echo "== Layer B — out-of-band trust anchor (manual — verify with the human) ==" +cat <<'EOF' + [ ] A SECOND, browser-independent source of truth for the address? + (emailed canonical address · short safety code/fingerprint · server-rendered identicon · EIP-55 checksum) + [ ] Is the QR generated SERVER-SIDE from the session (not from a client value the page can rewrite)? + [ ] Does "Copy" read a server-trusted JS value, not the rendered DOM text node? + [ ] Is the same safety code shown in 2+ places (header + receive page) so a single-surface swap looks inconsistent? + Residual (state plainly to security): a coherent MITB tamper of address+QR+code+identicon still wins + against a user who never checks the out-of-band channel. That floor is inherent to the web platform + (the reason hardware wallets exist) — document it; do not claim it is closed. +EOF +echo +echo "summary: $fails strict-CSP check(s) failed (Layer A). Layer B is the manual checklist above." +exit "$fails" diff --git a/vampi-out-patched/runs/20260727-031921/probes/forged-token.sh b/vampi-out-patched/runs/20260727-031921/probes/forged-token.sh new file mode 100644 index 0000000..875fbc3 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/forged-token.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +# forged-token — does this app actually VERIFY JWT signatures? Forge a token with a BOGUS +# signature + far-future exp and present it to each route that is GATED without auth. A route +# that returns 401/403 with NO token but REACHES THE HANDLER (200/400/404/422/…) WITH the +# forged token is trusting an UNVERIFIED token = authentication bypass (CWE-347 / OWASP API2). +# This is the dynamic VERDICT on a `decodeJwtPayloadUnsafe` / `jwt.decode(verify=False)` finding: +# the recon says "an unverified decoder feeds an auth decision"; this proves which routes fall. +# +# Read-only by default (GET routes). Set PROBE_WRITES=1 to ALSO test write verbs (empty body — +# non-destructive) — LOCALHOST/TEST only. Reads this app's routes from ./probe-context.json +# (written by websec). Tries Authorization: Bearer, plus a cookie if you pass COOKIE_NAME. +# Usage: TARGET=https://127.0.0.1:8443 [PROBE_WRITES=1] [COOKIE_NAME=session] bash forged-token.sh +set -uo pipefail +ctx="$(dirname "$0")/probe-context.json" +BASE="${TARGET:-$(python3 -c "import json;print(json.load(open('$ctx'))['target_base_url'])" 2>/dev/null)}" +if [ -z "${BASE:-}" ] || [ "${BASE#FILL}" != "$BASE" ]; then + echo "Set TARGET=http://host:port (or fill target_base_url in probe-context.json)"; exit 2 +fi +BASE="${BASE%/}" + +# A structurally-valid JWT with a DELIBERATELY INVALID signature + far-future exp. A correct +# verifier rejects this outright; a decode-only auth path trusts its claims. +FORGED=$(python3 -c " +import base64, json +def b(o): return base64.urlsafe_b64encode(json.dumps(o).encode()).rstrip(b'=').decode() +print(b({'alg':'RS256','typ':'JWT','kid':'forged'})+'.'+b({'sub':'websec-forged','email':'websec-forged@example.com','role':'admin','roles':['admin'],'exp':9999999999})+'.d2Vic2VjLWZvcmdlZC1zaWc') +") + +# Auth cookie names the app reads (from recon → probe-context.json) + an optional COOKIE_NAME +# override. We forge into these too, not just Authorization: Bearer, so a cookie-ONLY app isn't +# a false negative. (portable; macOS bash 3.2 lacks `mapfile`.) +COOKIES=() +[ -n "${COOKIE_NAME:-}" ] && COOKIES+=("$COOKIE_NAME") +while IFS= read -r cn; do [ -n "$cn" ] && COOKIES+=("$cn"); done < <(python3 -c " +import json +for c in json.load(open('$ctx')).get('auth',{}).get('cookie_names',[]): print(c) +" 2>/dev/null) + +# Routes to test: GET reads + GET idor/ssrf candidates (always); writes when PROBE_WRITES=1. +# Skip any path with an unfilled {param}. (portable; macOS bash 3.2 lacks `mapfile`.) +ROUTES=() +while IFS= read -r line; do [ -n "$line" ] && ROUTES+=("$line"); done < <(PROBE_WRITES="${PROBE_WRITES:-0}" python3 -c " +import json, os +c = json.load(open('$ctx')); eps = c['endpoints'] +rows = list(eps.get('reads', [])) +rows += [r.split(' ')[0] for r in eps.get('ssrf_candidates', [])] # 'GET /x (param: y)' -> 'GET /x' +rows += [r for r in eps.get('idor_candidates', []) if r.split(' ',1)[0] == 'GET'] +if os.environ.get('PROBE_WRITES') == '1': rows += eps.get('writes', []) +seen=set(); out=[] +for r in rows: + m = r.strip().split(' ', 1) + if len(m) != 2: continue + meth, path = m[0], m[1].split(' ')[0].strip() + if '{' in path or (meth, path) in seen: continue + seen.add((meth, path)); out.append(meth + ' ' + path) +print('\n'.join(out[:80])) +" 2>/dev/null) +if [ "${#ROUTES[@]}" -eq 0 ]; then + echo "No concrete (no-{param}) routes in probe-context.json to test."; exit 2 +fi + +# Codes that mean the request REACHED THE HANDLER (auth passed). Excludes 401/403 (blocked), +# 429 (rate-limited), 5xx/000 (ambiguous) so an aggressive limiter can't manufacture a bypass. +reached() { case "$1" in 200|201|202|203|204|206|400|404|405|409|413|415|422) return 0;; *) return 1;; esac; } + +echo "forged-token vs $BASE · unsigned/bogus-sig JWT, far-future exp" +echo " (a gated route that REACHES its handler with this token is NOT verifying the signature)" +echo "----------------------------------------------------------------------------------------------------" +bypass=0; ok=0; skip=0 +for ep in "${ROUTES[@]}"; do + method="${ep%% *}"; path="${ep#* }" + data=(); { [ "$method" != "GET" ] && [ "$method" != "HEAD" ]; } && data=(-H 'content-type: application/json' --data '{}') + na=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$BASE$path" ${data[@]+"${data[@]}"} --max-time 15) + if [ "$na" != "401" ] && [ "$na" != "403" ]; then skip=$((skip+1)); continue; fi # not gated unauthenticated → N/A here + fg=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$BASE$path" -H "Authorization: Bearer $FORGED" ${data[@]+"${data[@]}"} --max-time 15) + via="Bearer" + if ! reached "$fg"; then # Bearer didn't reach the handler → try forging into each known auth cookie + for cn in ${COOKIES[@]+"${COOKIES[@]}"}; do + cfg=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$BASE$path" -H "Cookie: $cn=$FORGED" ${data[@]+"${data[@]}"} --max-time 15) + if reached "$cfg"; then fg="$cfg"; via="cookie:$cn"; break; fi + done + fi + if reached "$fg"; then + printf ' BYPASS %s→%s %s %s (forged token accepted via %s)\n' "$na" "$fg" "$method" "$path" "$via"; bypass=$((bypass+1)) + else + printf ' ok %s→%s %s %s\n' "$na" "$fg" "$method" "$path"; ok=$((ok+1)) + fi +done +echo "----------------------------------------------------------------------------------------------------" +echo "summary: $bypass forged-token BYPASS · $ok rejected · $skip not-gated (skipped)" +if [ "$bypass" -gt 0 ]; then + echo "⚠ A token with NO valid signature reached the handler on $bypass route(s) — CWE-347 broken auth." + echo " Route the guard through a VERIFYING decode (jwt.verify with the key / a checked server session)," + echo " the same path your properly-protected routes use. Never trust a decode-only (\"Unsafe\") result." +fi +exit "$bypass" diff --git a/vampi-out-patched/runs/20260727-031921/probes/hs256-brute-force.py b/vampi-out-patched/runs/20260727-031921/probes/hs256-brute-force.py new file mode 100644 index 0000000..5adf42b --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/hs256-brute-force.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +""" +HS256 JWT secret brute-force probe. + +Tries a wordlist of common weak secrets against a captured JWT. If any +candidate verifies the signature, we have the secret -> can forge any +token -> critical finding. + +This is OFFLINE — no server requests. Run safely against any token. + +Usage: python3 hs256-brute-force.py +""" +import sys, hmac, hashlib, base64 + +if len(sys.argv) != 2: + print("Usage: hs256-brute-force.py ", file=sys.stderr) + sys.exit(2) + +token = sys.argv[1].strip() +try: + h_b64, p_b64, s_b64 = token.split('.') +except ValueError: + print("Token isn't a 3-part JWT", file=sys.stderr) + sys.exit(2) + +def b64url_pad(s): + return s + '=' * (-len(s) % 4) + +signed = (h_b64 + '.' + p_b64).encode() +expected_sig = base64.urlsafe_b64decode(b64url_pad(s_b64)) + +# Wordlist: common weak JWT secrets seen in the wild. +# Real bug bounties: "secret", "your-256-bit-secret" (the JWT.io default!), +# "jwt-secret", common framework defaults, project-name guesses. +# TODO: Add project-name candidates: variations of , the company +# name, internal team names, any nickname you've seen in docs. Attackers will. +CANDIDATES = [ + # JWT.io default (used by tutorials, sometimes makes it to production) + "your-256-bit-secret", + # NextAuth + Auth.js defaults + "your-secret-key", "your-secret", + # Express + jsonwebtoken tutorials + "secret", "jwt-secret", "jwtsecret", "JWT_SECRET", + "supersecret", "supersecretkey", + # Common dev placeholders + "changeme", "changeit", "changethis", "CHANGEME", + "password", "Password1!", "admin", "admin123", + "test", "test123", "testing", + "dev", "development", "local", + "default", "default-secret", + # PROJECT-SPECIFIC START + # TODO: project-name and company-name guesses go here. + # "", "", "", + # PROJECT-SPECIFIC END + # Common framework env defaults + "your-secret-jwt-key", "secretkey", + # Length-32 placeholders + "0123456789abcdef0123456789abcdef", + "abcdefghijklmnopqrstuvwxyz123456", + # Empty/null secrets (sometimes accepted) + "", + "null", "undefined", + # Common from leaked-secret datasets + "123456", "12345678", "qwerty", +] + +print(f"=== HS256 brute force test ===") +print(f" Token header.payload length: {len(h_b64) + 1 + len(p_b64)} bytes") +print(f" Trying {len(CANDIDATES)} candidate secrets (offline, no requests)...") +print() + +found = None +for cand in CANDIDATES: + computed = hmac.new(cand.encode(), signed, hashlib.sha256).digest() + if hmac.compare_digest(computed, expected_sig): + found = cand + break + +if found is not None: + print(f" !! CRITICAL: signing secret cracked!") + print(f" Secret value: {'' if not found else repr(found)}") + print(f" Implication: any attacker can forge JWTs for any user/role.") + print(f" Action: ROTATE the JWT signing secret IMMEDIATELY.") + sys.exit(1) +else: + print(f" OK -- none of {len(CANDIDATES)} weak-secret candidates work.") + print(f" This does NOT prove the secret is strong -- only that it's not in") + print(f" this short wordlist. For a real engagement, use hashcat mode 16500") + print(f" against rockyou.txt or larger lists.") + sys.exit(0) diff --git a/vampi-out-patched/runs/20260727-031921/probes/jwt-attacks.sh b/vampi-out-patched/runs/20260727-031921/probes/jwt-attacks.sh new file mode 100644 index 0000000..f3d1463 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/jwt-attacks.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +# jwt-attacks.sh — manual JWT attack probe (FACTS-driven; no app-specific login). +# +# Five classic JWT attacks, run against a protected endpoint with a token YOU supply: +# 1. alg:none — if accepted, total auth bypass. 2. tampered claims + wrong HS256 sig. +# 3. expired exp. 4. stripped signature. 5. garbage token. (each should 401/403) +# Optional 6. refresh-replay-after-logout if you set REFRESH_TOKEN + the routes exist. +# +# Env (see _lib.py): TARGET, TOKEN_A=. +# Optional: TEST_PATH=/api/some/protected/route (else picked from probe-context.json), +# REFRESH_TOKEN, LOGOUT_PATH, REFRESH_PATH. Run only against a TEST instance. +set -uo pipefail +cd "$(dirname "$0")" +ctx=probe-context.json + +TARGET="${TARGET:-$(python3 -c "import json;print(json.load(open('$ctx'))['target_base_url'])" 2>/dev/null)}" +if [ -z "${TARGET:-}" ] || [ "${TARGET#FILL}" != "$TARGET" ]; then echo "Set TARGET=http://host:port (or fill probe-context.json)"; exit 2; fi +: "${TOKEN_A:?set TOKEN_A=}" +ACCESS_TOKEN="$TOKEN_A" +# a protected endpoint to fire forged tokens at (override with TEST_PATH) +TEST_PATH="${TEST_PATH:-$(python3 -c "import json;c=json.load(open('$ctx'))['endpoints'];print((c.get('idor_candidates') or c.get('writes') or ['/']).__getitem__(0).split(' ',1)[-1])" 2>/dev/null)}" +TEST_URL="$TARGET${TEST_PATH:-/}" + +b64url() { python3 -c "import sys,base64; sys.stdout.write(base64.urlsafe_b64encode(sys.stdin.buffer.read()).decode().rstrip('='))"; } +IFS='.' read -r H P S <<< "$ACCESS_TOKEN" +PASS_COUNT=0; FAIL_COUNT=0; FAIL_LINES=() +check() { + if [ "$3" = "$2" ]; then printf ' PASS %-28s expected:%s actual:%s\n' "$1" "$2" "$3"; PASS_COUNT=$((PASS_COUNT+1)); + else printf ' FAIL %-28s expected:%s actual:%s\n' "$1" "$2" "$3"; FAIL_COUNT=$((FAIL_COUNT+1)); FAIL_LINES+=("$1 expected $2 got $3"); fi +} +echo "=== JWT attacks vs $TEST_URL ===" +code=$(curl -s -o /dev/null -w '%{http_code}' "$TEST_URL" -H "Authorization: Bearer $ACCESS_TOKEN"); check "sanity (legit token)" "200" "$code" +DECODED_P=$(echo "$P" | python3 -c "import sys,base64; d=sys.stdin.read(); print(base64.urlsafe_b64decode(d+'=='*(4-len(d)%4)).decode())" 2>/dev/null || echo '{}') + +NEW_H=$(printf '{"alg":"none","typ":"JWT"}' | b64url); code=$(curl -s -o /dev/null -w '%{http_code}' "$TEST_URL" -H "Authorization: Bearer ${NEW_H}.${P}."); check "alg:none bypass" "401" "$code" +HS=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url) +TP=$(printf '%s' "$DECODED_P" | python3 -c "import sys,json,time +try: d=json.loads(sys.stdin.read() or '{}') +except Exception: d={} +d['admin']=True; d['exp']=int(time.time())+3600 +print(json.dumps(d))" 2>/dev/null || echo '{}') +TPB=$(printf '%s' "$TP" | b64url) +WSIG=$(printf '%s.%s' "$HS" "$TPB" | python3 -c "import sys,hmac,hashlib,base64; print(base64.urlsafe_b64encode(hmac.new(b'wrong-secret',sys.stdin.buffer.read(),hashlib.sha256).digest()).decode().rstrip('='))") +code=$(curl -s -o /dev/null -w '%{http_code}' "$TEST_URL" -H "Authorization: Bearer ${HS}.${TPB}.${WSIG}"); check "tampered claims + wrong sig" "401" "$code" +EP=$(echo "$DECODED_P" | python3 -c "import sys,json,time; +try: d=json.loads(sys.stdin.read()) +except: d={} +d['exp']=int(time.time())-60; print(json.dumps(d))" 2>/dev/null || echo '{}') +EPB=$(printf '%s' "$EP" | b64url); code=$(curl -s -o /dev/null -w '%{http_code}' "$TEST_URL" -H "Authorization: Bearer ${H}.${EPB}.${S}"); check "expired exp" "401" "$code" +code=$(curl -s -o /dev/null -w '%{http_code}' "$TEST_URL" -H "Authorization: Bearer ${H}.${P}."); check "stripped signature" "401" "$code" +code=$(curl -s -o /dev/null -w '%{http_code}' "$TEST_URL" -H "Authorization: Bearer not-a-jwt"); check "garbage token" "401" "$code" + +if [ -n "${REFRESH_TOKEN:-}" ]; then + curl -s -o /dev/null -X POST "$TARGET${LOGOUT_PATH:-/api/auth/logout}" -H "Authorization: Bearer $ACCESS_TOKEN" -H 'content-type: application/json' -d "{\"refreshToken\":\"$REFRESH_TOKEN\"}" || true + code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$TARGET${REFRESH_PATH:-/api/auth/refresh}" -H 'content-type: application/json' -d "{\"refreshToken\":\"$REFRESH_TOKEN\"}") + [ "$code" = "401" ] && echo " PASS refresh-after-logout (invalidated)" || echo " WARN refresh-after-logout actual:$code (stateless replay? document the tradeoff)" +fi + +echo "=== Summary: PASS=$PASS_COUNT FAIL=$FAIL_COUNT ===" +[ "$FAIL_COUNT" -gt 0 ] && { printf ' - %s\n' "${FAIL_LINES[@]}"; exit 1; } +echo "All JWT attacks blocked — auth layer holds." diff --git a/vampi-out-patched/runs/20260727-031921/probes/mass-assignment.py b/vampi-out-patched/runs/20260727-031921/probes/mass-assignment.py new file mode 100644 index 0000000..3d005fd --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/mass-assignment.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +"""Mass assignment / BOPLA (OWASP API #3) — FACTS-driven. + +Injects THIS app's privileged model fields (from probe-context.json → sensitive_fields, +i.e. the schema extractor's output: role/isAdmin/groupId/ownerId/…) into each write +endpoint and flags any request that's ACCEPTED (2xx). A correct server strips or rejects +server-controlled fields. Acceptance is a *lead*, not proof — re-fetch the object as the +agent to confirm the privileged field actually persisted/escalated before reporting. + +Env (see _lib.py): TARGET, TOKEN_A|COOKIE_A|APIKEY, optional OBJ_A (your own object id for +self-edit paths; defaults to the literal 'me'). +""" +import os +import re +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import _lib # noqa: E402 + +BASE = _lib.base_url() +HDR = _lib.auth_headers("A") +if not HDR: + sys.exit("Supply auth: TOKEN_A= (or COOKIE_A / APIKEY). See _lib.py.") +OBJ_A = os.environ.get("OBJ_A", "me") +fields = _lib.context().get("sensitive_fields") or ["role", "isAdmin", "groupId", "ownerId", "permissions"] + + +def _val(f: str): + fl = f.lower() + if any(k in fl for k in ("isadmin", "admin", "verified", "enabled", "active")): + return True + if any(k in fl for k in ("permission", "scope", "group", "roles")): + return ["*"] + if any(k in fl for k in ("role", "status", "plan", "tier")): + return "admin" + return "websec-injected" + + +PAYLOAD = {f: _val(f) for f in fields} +eps = _lib.write_endpoints() +if not eps: + sys.exit("No write endpoints in probe-context.json — nothing to probe.") + +print(f"=== Mass-assignment probe vs {BASE} injecting app fields: {list(PAYLOAD)} ===") +findings = [] +for method, path in eps: + url = BASE + re.sub(r"\{[^}]+\}", OBJ_A, path) + code, body = _lib.curl(method, url, headers=HDR, body=dict(PAYLOAD)) + sev = "SUSPECT" if code in (200, 201, 204) else ("PASS" if code in (400, 403, 422) else "INVESTIGATE") + mark = {"SUSPECT": "!!", "PASS": "ok", "INVESTIGATE": "??"}[sev] + findings.append({"method": method, "path": path, "url": url, "status": code, + "severity": sev, "preview": body[:140]}) + print(f" [{mark}] {sev:11} {method:6} {url} -> {code}") + +out = _lib.save("mass-assignment", findings) +sus = sum(1 for f in findings if f["severity"] == "SUSPECT") +print(f"\n SUSPECT (privileged fields accepted — verify they stuck)={sus} · saved {out.name}") +print(" Re-fetch the object as the agent to confirm the field persisted/escalated, then debate-verify.") +sys.exit(1 if sus else 0) diff --git a/vampi-out-patched/runs/20260727-031921/probes/probe-context.json b/vampi-out-patched/runs/20260727-031921/probes/probe-context.json new file mode 100644 index 0000000..2d3457a --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/probe-context.json @@ -0,0 +1,48 @@ +{ + "target_base_url": "FILL_ME (e.g. http://localhost:3000)", + "auth": { + "scheme": "jwt (bearer)", + "token_location": "bearer", + "cookie_names": [], + "login_endpoints": [ + "POST /users/v1/login" + ], + "how_to_authenticate": "cookie-session (e.g. NextAuth) \u2192 send the session cookie; bearer \u2192 Authorization: Bearer ; api-key \u2192 the documented key header" + }, + "endpoints": { + "writes": [ + "POST /books/v1", + "DELETE /users/v1/{username}", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password", + "POST /users/v1/login", + "POST /users/v1/register" + ], + "reads": [ + "GET /", + "GET /books/v1", + "GET /books/v1/{book_title}", + "GET /createdb", + "GET /me", + "GET /users/v1", + "GET /users/v1/{username}", + "GET /users/v1/_debug" + ], + "idor_candidates": [ + "DELETE /users/v1/{username}", + "GET /books/v1/{book_title}", + "GET /users/v1/{username}", + "PUT /users/v1/{username}/email", + "PUT /users/v1/{username}/password" + ], + "ssrf_candidates": [], + "upload_candidates": [], + "auth_endpoints": [ + "POST /users/v1/login" + ] + }, + "sensitive_fields": [], + "tenant_keys": [], + "datastore_class": "sql", + "note": "These are THIS app's real routes/auth (from FACTS.json). Finalize each probe draft against this file, supply secrets/tokens, then run against a TEST instance only." +} diff --git a/vampi-out-patched/runs/20260727-031921/probes/race-conditions.py b/vampi-out-patched/runs/20260727-031921/probes/race-conditions.py new file mode 100644 index 0000000..d5dec2a --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/race-conditions.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +""" +Race condition probe — fires N parallel requests at race-prone endpoints +and checks if the server's invariants hold. + +Common race targets: + - "claim" / "assign" endpoints — only one parallel claim should succeed + - status / state toggles — multiple parallel calls should converge + - inventory / quota decrements — should not allow over-spend + - tag/label-add endpoints — should dedupe + +For each target: + - Fire PARALLEL_REQUESTS in parallel + - Count successes (200/201) + - Compare to expected_unique (usually 1 — only one assignment should win) + - If success_count > expected_unique -> race condition likely + +Uses async httpx for true parallelism (synchronous loops can't trigger races). + +Install: pip install httpx +""" +import asyncio, httpx, json, os, re, sys +from pathlib import Path +from collections import Counter + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import _lib # noqa: E402 + +TARGET = _lib.base_url() +_lib.require("OBJ_A") # an object id you own (the single-winner target) +OBJ_A = os.environ["OBJ_A"] +_tok, _cookie = os.environ.get("TOKEN_A"), os.environ.get("COOKIE_A") +HEADERS = {"Authorization": f"Bearer {_tok}"} if _tok else ({"Cookie": _cookie} if _cookie else {}) +if not HEADERS: + sys.exit("Supply auth: TOKEN_A= (or COOKIE_A). See _lib.py.") + +PARALLEL = 50 # concurrent requests per target + +# Race-prone targets = this app's mutating endpoints (from probe-context.json). For each, the +# server should keep its single-winner / converge / dedupe invariant under PARALLEL concurrency. +TARGETS = [{"name": f"{m} {p}", "method": m, "url": TARGET + re.sub(r"\{[^}]+\}", OBJ_A, p), + "payload": {}, "expected_unique": 1, + "note": "parallel fire — a single-winner/converge/dedupe invariant should hold"} + for m, p in _lib.write_endpoints()][:8] +if not TARGETS: + sys.exit("No write endpoints in probe-context.json — nothing to probe.") + +async def fire(client, t): + """Single request, return (status_code, response_body_preview)""" + try: + r = await client.request( + t['method'], t['url'], + json=t['payload'], + headers=HEADERS, + timeout=30.0, + ) + return (r.status_code, r.text[:120]) + except Exception as e: + return (None, str(e)[:120]) + +async def run_target(t): + print(f" Firing {PARALLEL} parallel {t['method']} to {t['url'][len(TARGET):]}") + async with httpx.AsyncClient() as client: + results = await asyncio.gather(*[fire(client, t) for _ in range(PARALLEL)]) + codes = Counter(r[0] for r in results) + success = sum(1 for r in results if r[0] and 200 <= r[0] < 300) + race_likely = success > t['expected_unique'] + print(f" -> status counts: {dict(codes)}, successes: {success}, expected: {t['expected_unique']}") + if race_likely: + print(f" !! RACE CONDITION SUSPECTED -- {success} successes vs {t['expected_unique']} expected") + return { + 'name': t['name'], + 'parallel': PARALLEL, + 'status_counts': dict(codes), + 'success_count': success, + 'expected_unique': t['expected_unique'], + 'race_suspected': race_likely, + 'note': t['note'], + 'sample_responses': [r for r in results if r[0] and r[0] < 500][:3], + } + +async def main(): + findings = [] + for t in TARGETS: + print(f"\n=== {t['name']}: {t['note']}") + f = await run_target(t) + findings.append(f) + out = _lib.save("race-conditions", findings) + crit = sum(1 for f in findings if f['race_suspected']) + print(f"\n=== Summary ===") + print(f" race suspected on {crit}/{len(findings)} endpoints") + print(f" saved to {out}") + return crit + +if __name__ == '__main__': + rc = asyncio.run(main()) + sys.exit(1 if rc > 0 else 0) diff --git a/vampi-out-patched/runs/20260727-031921/probes/rate-limit-burst.sh b/vampi-out-patched/runs/20260727-031921/probes/rate-limit-burst.sh new file mode 100644 index 0000000..ca8ef40 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/rate-limit-burst.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +# rate-limit-burst — verify rate limiters actually fire, and that they can't be bypassed by +# spoofing X-Forwarded-For. FACTS-driven: reads the login route + base URL from +# ./probe-context.json (written by websec) — no separate .env needed. +# +# Three tests: +# 1. AUTH limiter — N+1 failed logins; expect a 429 by attempt N+1. (A limit of N ALLOWS N and +# blocks the N+1th, so sending only N false-FAILs a working limiter — the classic off-by-one.) +# 2. General limiter — burst of GETs at a public endpoint; expect 429s once over the per-IP budget. +# 3. XFF bypass — once limited, rotate X-Forwarded-For between requests. If the limit lifts, the +# backend keys on a client-controlled header without verifying the proxy chain (bypassable). +# +# Env: TARGET (or target_base_url in probe-context.json). Optional overrides: +# AUTH_LIMIT (default 10), LOGIN_PATH, HEALTH_PATH. +# Usage: TARGET=http://localhost:3000 bash rate-limit-burst.sh +set -uo pipefail +ctx="$(dirname "$0")/probe-context.json" +BASE="${TARGET:-$(python3 -c "import json;print(json.load(open('$ctx'))['target_base_url'])" 2>/dev/null)}" +if [ -z "${BASE:-}" ] || [ "${BASE#FILL}" != "$BASE" ]; then + echo "Set TARGET=http://host:port (or fill target_base_url in probe-context.json)"; exit 2 +fi +BASE="${BASE%/}" + +# Login path: explicit override → the POST .../login from probe-context → a sane default. +LOGIN_PATH="${LOGIN_PATH:-$(python3 -c " +import json +c = json.load(open('$ctx')) +eps = c.get('auth', {}).get('login_endpoints', []) + c.get('endpoints', {}).get('auth_endpoints', []) +cand = [e.split(' ', 1)[1] for e in eps if e.upper().startswith('POST ') and 'login' in e.lower()] +print(cand[0] if cand else '/api/auth/login') +" 2>/dev/null)}" +LOGIN_PATH="${LOGIN_PATH:-/api/auth/login}" +HEALTH_PATH="${HEALTH_PATH:-/api/health}" +LIMIT="${AUTH_LIMIT:-10}" +N=$((LIMIT + 1)) # N+1: a limit of N allows N and blocks the (N+1)th + +fails=0 + +echo "=== Test 1: AUTH limiter — $N failed logins at $LOGIN_PATH (expect a 429 by #$N) ===" +saw429=0 +for i in $(seq 1 "$N"); do + code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE$LOGIN_PATH" \ + -H 'content-type: application/json' --data '{"email":"rl-test@example.com","password":"wrong"}' --max-time 15) + printf ' attempt %2d → %s\n' "$i" "$code" + [ "$code" = "429" ] && saw429=1 +done +if [ "$saw429" = "1" ]; then + echo " PASS AUTH limiter fired (saw 429)" +else + echo " FAIL AUTH limiter never fired in $N attempts — misconfigured, or the limit is > $LIMIT (raise AUTH_LIMIT)" + fails=$((fails+1)) +fi +echo + +echo "=== Test 2: general limiter — 200 GET $HEALTH_PATH in ~10s ===" +codes=$(seq 1 200 | xargs -n1 -P20 -I{} curl -s -o /dev/null -w '%{http_code}\n' "$BASE$HEALTH_PATH" --max-time 15) +n429=$(printf '%s\n' "$codes" | grep -c '^429$' || true) +n200=$(printf '%s\n' "$codes" | grep -c '^200$' || true) +echo " 200: $n200 · 429: $n429" +if [ "$n429" -gt 0 ]; then echo " INFO general limiter fires under burst"; else + echo " INFO general limiter did not fire at 200 reqs — below threshold (raise for a real pentest)"; fi +echo + +echo "=== Test 3: X-Forwarded-For spoof bypass ===" +for i in $(seq 1 "$N"); do + curl -s -o /dev/null -X POST "$BASE$LOGIN_PATH" -H 'content-type: application/json' \ + --data '{"email":"xff-test@example.com","password":"wrong"}' --max-time 15 || true +done +baseline=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE$LOGIN_PATH" \ + -H 'content-type: application/json' --data '{"email":"xff-test@example.com","password":"wrong"}' --max-time 15) +echo " baseline (no XFF): $baseline" +spoofed=0 +for xff in "1.2.3.4" "10.0.0.1" "192.168.1.99" "127.0.0.1" "1.1.1.1, 2.2.2.2"; do + code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE$LOGIN_PATH" -H "X-Forwarded-For: $xff" \ + -H 'content-type: application/json' --data '{"email":"xff-test@example.com","password":"wrong"}' --max-time 15) + printf ' XFF=%-22s → %s\n' "$xff" "$code" + { [ "$baseline" = "429" ] && [ "$code" != "429" ]; } && spoofed=$((spoofed+1)) +done +if [ "$baseline" != "429" ]; then + echo " SKIP limiter not in 429 state for the baseline — can't test bypass (raise AUTH_LIMIT or the window)" +elif [ "$spoofed" -gt 0 ]; then + echo " FAIL XFF spoof bypassed the limiter ($spoofed/5) — it keys on client-supplied XFF without verifying the proxy chain" + fails=$((fails+1)) +else + echo " PASS XFF spoof did NOT bypass the limiter (all stayed 429)" +fi +echo +echo "=== summary: $fails failure(s) ===" +exit "$fails" diff --git a/vampi-out-patched/runs/20260727-031921/probes/unauth-baseline.sh b/vampi-out-patched/runs/20260727-031921/probes/unauth-baseline.sh new file mode 100644 index 0000000..b66ceb1 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/unauth-baseline.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +# unauth-baseline — the cheapest, highest-value probe: hit every MUTATING route with +# NO credentials and expect 401/403. Any 2xx (or a non-401 that reached the handler) +# is a missing-authentication lead. Run this FIRST — it confirms the auth model before +# you spend effort on authorization/BOLA probes, and it catches both failure modes: +# a genuinely-open endpoint, AND an app whose auth fails OPEN in the test env (see below). +# +# Reads the target's real write routes from ./probe-context.json (written by websec). +# Usage: TARGET=http://localhost:3000 bash unauth-baseline.sh +set -uo pipefail + +ctx="$(dirname "$0")/probe-context.json" +BASE="${TARGET:-$(python3 -c "import json;print(json.load(open('$ctx'))['target_base_url'])" 2>/dev/null)}" +if [ -z "${BASE:-}" ] || [ "${BASE#FILL}" != "$BASE" ]; then + echo "Set TARGET=http://host:port (or fill target_base_url in probe-context.json)"; exit 2 +fi + +EPS=() # (portable; macOS bash 3.2 lacks `mapfile`) +while IFS= read -r line; do [ -n "$line" ] && EPS+=("$line"); done < <(python3 -c "import json;[print(e) for e in json.load(open('$ctx'))['endpoints']['writes']]" 2>/dev/null) +if [ "${#EPS[@]}" -eq 0 ]; then + echo "No write endpoints in probe-context.json — add 'METHOD /path' lines under endpoints.writes."; exit 2 +fi + +echo "unauth baseline vs $BASE (no credentials sent; each SHOULD be 401/403)" +echo "------------------------------------------------------------------------" +leads=0 ok=0 +for ep in "${EPS[@]}"; do + method="${ep%% *}"; path="${ep#* }" + code=$(curl -s -o /dev/null -w '%{http_code}' -X "$method" "$BASE$path" \ + -H 'content-type: application/json' --data '{}' --max-time 15) + case "$code" in + 401|403) printf ' ok %s %s %s\n' "$code" "$method" "$path"; ok=$((ok+1)) ;; + 000) printf ' ???? conn-fail %s %s (is the app running?)\n' "$method" "$path" ;; + *) printf ' LEAD %s %s %s ← reached WITHOUT auth — verify\n' "$code" "$method" "$path"; leads=$((leads+1)) ;; + esac +done +echo "------------------------------------------------------------------------" +echo "summary: $ok enforced (401/403) · $leads lead(s) reached without auth" +if [ "$ok" -eq 0 ] && [ "${#EPS[@]}" -gt 1 ]; then + echo "⚠ EVERY route was reachable unauthenticated. Before concluding 'no auth', RULE OUT a" + echo " fail-OPEN test env: if the auth provider (Cognito/Auth0/etc.) isn't configured, the" + echo " middleware may be erroring through. Configure a valid/dummy provider (or mock a" + echo " session) and re-run — if these flip to 401, the app is fine and the env was the bug." +fi +exit "$leads" diff --git a/vampi-out-patched/runs/20260727-031921/probes/webhook-forgery.py b/vampi-out-patched/runs/20260727-031921/probes/webhook-forgery.py new file mode 100644 index 0000000..ac666a4 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/probes/webhook-forgery.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# ───────────────────────────────────────────────────────────────────────────── +# websec-validator — DRAFT probe. Any example endpoints / auth / login below are +# PLACEHOLDERS from the template. THIS target's real surface — routes, auth scheme +# + token location, sensitive fields, tenant key — is in ./probe-context.json +# (generated from FACTS.json for this app). Use those values before running; the +# agent should finalize this draft against probe-context.json, then fill secrets. +# ───────────────────────────────────────────────────────────────────────────── +# ⚠ DEFENSIVE CHECK — run only against a system you own/operate, with consent. Not for production or third-party targets. +""" +Webhook forgery probe — signature verification for inbound webhooks. + +A correct webhook verifier uses: + - crypto.timingSafeEqual (or HMAC compare_digest) — not raw == comparison + - fail-closed — reject if ANY required header is missing or malformed + - timestamp-age check — reject signatures older than ~5 minutes to prevent + captured-and-replayed-later forgeries + +This probe tests: + 1. No signature header -> expect 401 + 2. Invalid signature (random b64) -> expect 401 + 3. Garbage signature (non-b64) -> expect 401 + 4. Missing timestamp -> expect 401 + 5. Far-future timestamp -> expect 401 ideally (replay-window check) + 6. Far-past timestamp -> same + 7. Truncated signature -> expect 401 + 8. Empty body -> expect 401 + 9. Wrong content-type -> expect 401 +""" +import json, os, subprocess, time, sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import _lib # noqa: E402 + +TARGET = _lib.base_url() + +# PROJECT-SPECIFIC START +# TODO: replace with your project's inbound-webhook path, signature header +# name, and timestamp header name. Examples: +# Bird / MessageBird: /webhooks/messagebird, messagebird-signature, messagebird-timestamp +# Stripe: /webhooks/stripe, Stripe-Signature (combined ts+sig) +# Twilio: /webhooks/twilio, X-Twilio-Signature +# GitHub: /webhooks/github, X-Hub-Signature-256 +# Custom: /webhooks/, X-Signature, X-Timestamp +# set WEBHOOK_PATH / SIG_HEADER / TS_HEADER env vars, or edit these. probe-context.json +# lists this app's detected integrations/webhooks to pick the real path from. +WEBHOOK_PATH = os.environ.get("WEBHOOK_PATH", "/webhooks/") +SIG_HEADER = os.environ.get("SIG_HEADER", "x-signature") +TS_HEADER = os.environ.get("TS_HEADER", "x-timestamp") + +URL = f"{TARGET}{WEBHOOK_PATH}" + +# TODO: realistic payload shape for your provider. +PAYLOAD = json.dumps({ + "event": "message.received", + "type": "message", + "channelId": "channel-id-xxx", + "message": { + "id": "fake-msg-id", + "from": "+15551234567", + "content": "hello from attacker", + } +}) +# PROJECT-SPECIFIC END + +probes = [ + # (name, headers, body, expected_code, expected_reason) + ('no-signature', {}, PAYLOAD, 401, 'no sig'), + ('invalid-signature-b64', {SIG_HEADER: 'aW52YWxpZA=='}, PAYLOAD, 401, 'bad sig'), + ('garbage-signature', {SIG_HEADER: 'not-base64-!'}, PAYLOAD, 401, 'malformed sig'), + ('missing-timestamp', {SIG_HEADER: 'aW52YWxpZA=='}, PAYLOAD, 401, 'no timestamp'), + ('zero-timestamp', {SIG_HEADER: 'aW52YWxpZA==', TS_HEADER: '0'}, PAYLOAD, 401, 'timestamp epoch 0'), + ('far-future-timestamp', {SIG_HEADER: 'aW52YWxpZA==', TS_HEADER: '4070908800'}, PAYLOAD, 401, 'timestamp year 2099'), + ('far-past-timestamp', {SIG_HEADER: 'aW52YWxpZA==', TS_HEADER: '1000000000'}, PAYLOAD, 401, 'timestamp year 2001'), + ('truncated-signature', {SIG_HEADER: 'a'}, PAYLOAD, 401, 'too short'), + ('empty-body', {SIG_HEADER: 'aW52YWxpZA==', TS_HEADER: str(int(time.time()))}, '', 401, 'empty body'), + ('wrong-content-type', {SIG_HEADER: 'aW52YWxpZA==', TS_HEADER: str(int(time.time())), 'Content-Type': 'text/plain'}, PAYLOAD, 401, 'wrong ct'), +] + +findings = [] +print(f"=== Webhook forgery probes against {URL} ===\n") + +for name, headers, body, expected, reason in probes: + cmd = ['curl', '-s', '-X', 'POST', URL, '-w', '\nHTTP_CODE:%{http_code}'] + for h, v in headers.items(): + cmd += ['-H', f'{h}: {v}'] + if 'Content-Type' not in headers: + cmd += ['-H', 'Content-Type: application/json'] + cmd += ['-d', body] + r = subprocess.run(cmd, capture_output=True, text=True) + out = r.stdout + code = int(out.split('HTTP_CODE:')[-1].strip()) if 'HTTP_CODE:' in out else 0 + body_text = out.split('\nHTTP_CODE:')[0] + expected_ok = code == expected + mark = 'OK' if expected_ok else '!!' + sev = 'PASS' if expected_ok else 'FAIL' + print(f" [{mark}] [{sev}] {name:30s} expected={expected} actual={code} ({reason})") + findings.append({ + 'name': name, 'expected': expected, 'actual': code, 'pass': expected_ok, + 'body_preview': body_text[:120], + }) + +out_p = _lib.save("webhook-forgery", findings) + +passed = sum(1 for f in findings if f['pass']) +print(f"\n=== Summary ===") +print(f" {passed}/{len(findings)} probes returned expected 401") +print(f" Saved: {out_p}") + +# Replay-window note +print() +print("=== Note on timestamp-age / replay window ===") +print(" Even if the HMAC is correct, captured webhooks should not replay forever.") +print(" Look in your handler for code like:") +print(" const age = Math.abs(Date.now()/1000 - parseInt(timestamp));") +print(" if (age > 300) return res.status(401).json({error:'webhook timestamp out of window'});") +print(" If that check is missing, log it as a finding (low severity, easy fix).") diff --git a/vampi-out-patched/runs/20260727-031921/results.sarif b/vampi-out-patched/runs/20260727-031921/results.sarif new file mode 100644 index 0000000..572c4c4 --- /dev/null +++ b/vampi-out-patched/runs/20260727-031921/results.sarif @@ -0,0 +1,701 @@ +{ + "version": "2.1.0", + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "websec-validator", + "informationUri": "https://github.com/raccioly/websec-validator", + "version": "0.11.0", + "rules": [ + { + "id": "websec/missing-auth", + "name": "MissingAuth", + "shortDescription": { + "text": "CWE-862 Missing Authorization" + }, + "fullDescription": { + "text": "Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten." + }, + "helpUri": "https://github.com/raccioly/websec-validator", + "properties": { + "attack_class": "missing-auth", + "cwe": [ + "CWE-862 Missing Authorization", + "CWE-306 Missing Authentication" + ], + "asvs": "ASVS V4.1.1", + "owasp_api": [ + "API1:2023 BOLA", + "API5:2023 BFLA" + ], + "tags": [ + "security", + "CWE-862", + "CWE-306" + ], + "security-severity": "8.0" + } + }, + { + "id": "websec/iac", + "name": "Iac", + "shortDescription": { + "text": "CWE-1188 Insecure Default" + }, + "fullDescription": { + "text": "Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.)." + }, + "helpUri": "https://github.com/raccioly/websec-validator", + "properties": { + "attack_class": "iac", + "cwe": [ + "CWE-1188 Insecure Default" + ], + "asvs": "ASVS V14.1", + "owasp_api": [], + "tags": [ + "security", + "CWE-1188" + ], + "security-severity": "5.5" + } + }, + { + "id": "websec/incomplete-hsts", + "name": "IncompleteHsts", + "shortDescription": { + "text": "CWE-523 Unprotected Transport of Credentials" + }, + "fullDescription": { + "text": "Apply HSTS uniformly at the EDGE to ALL responses (not just /api): `max-age>=31536000; includeSubDomains; preload` where the domain model allows." + }, + "helpUri": "https://github.com/raccioly/websec-validator", + "properties": { + "attack_class": "incomplete-hsts", + "cwe": [ + "CWE-523 Unprotected Transport of Credentials", + "CWE-319 Cleartext Transmission of Sensitive Information" + ], + "asvs": "ASVS V9.1.2", + "owasp_api": [ + "API8:2023 Misconfiguration" + ], + "tags": [ + "security", + "CWE-523", + "CWE-319" + ], + "security-severity": "3.0" + } + } + ] + } + }, + "properties": { + "schema_version": "1.0", + "target": "/home/jules/.cache/websec-corpus/VAmPI", + "total": 16, + "by_severity": { + "HIGH": 4, + "MEDIUM": 10, + "LOW": 2 + } + }, + "results": [ + { + "ruleId": "websec/missing-auth", + "level": "error", + "message": { + "text": "Missing authorization: POST /books/v1\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "ccda9cf549971c16" + }, + "properties": { + "severity": "HIGH", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "8.0", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/books/v1" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "error", + "message": { + "text": "Missing authorization: DELETE /users/v1/{username}\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "c32968d10958c9c3" + }, + "properties": { + "severity": "HIGH", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "8.0", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/users/v1/{username}" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "error", + "message": { + "text": "Missing authorization: PUT /users/v1/{username}/email\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "f641e6b943ade7ec" + }, + "properties": { + "severity": "HIGH", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "8.0", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/users/v1/{username}/email" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "error", + "message": { + "text": "Missing authorization: PUT /users/v1/{username}/password\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "97e214fdac4029de" + }, + "properties": { + "severity": "HIGH", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "8.0", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/users/v1/{username}/password" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "52f7d8ce874b5b5d" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /books/v1\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "c76a62be79e44ab2" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/books/v1" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /books/v1/{book_title}\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "82e88da3362d05d9" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/books/v1/{book_title}" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /createdb\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "d39911c87724a075" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/createdb" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /me\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "b94f3c9ff43eae6d" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/me" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /users/v1\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "920c877709b8d1e5" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/users/v1" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /users/v1/{username}\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "ab2771d8e049e06b" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/users/v1/{username}" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/missing-auth", + "level": "warning", + "message": { + "text": "Missing authorization: GET /users/v1/_debug\n\nno auth guard found in handler openapi_specs/openapi3.yml\n\nRemediation: Add an auth guard to the handler (e.g. requireAuth()/getServerSession()), or a middleware matcher over /api/(.*) with an explicit public allowlist so it can't be forgotten.\n\nCalibrated P(real)=0.659 (basis=class+label, n=41)." + }, + "partialFingerprints": { + "websecFingerprintV1": "8f4ce95eb01b6b73" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "access-control", + "calibrated": { + "p": 0.659, + "ci": [ + 0.505, + 0.784 + ], + "n": 41, + "basis": "class+label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/users/v1/_debug" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/iac", + "level": "warning", + "message": { + "text": "gha-unpinned-action: actions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@\n\nactions pinned to a mutable tag (pin to a commit SHA): docker/build-push-action@v3, docker/login-action@v2, docker/setup-buildx-action@v2, docker/setup-qemu-action@v2\n\nRemediation: Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).\n\nCalibrated P(real)=0.569 (basis=label, n=51)." + }, + "partialFingerprints": { + "websecFingerprintV1": "89e7cb3176361d71" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "iac-ci", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": ".github/workflows/docker-image.yml" + }, + "region": { + "startLine": 1 + } + } + } + ] + }, + { + "ruleId": "websec/iac", + "level": "warning", + "message": { + "text": "docker-root: container runs as root (add a non-root USER)\n\ncontainer runs as root (add a non-root USER)\n\nRemediation: Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).\n\nCalibrated P(real)=0.569 (basis=label, n=51)." + }, + "partialFingerprints": { + "websecFingerprintV1": "dfc2c51eeac06ce9" + }, + "properties": { + "severity": "MEDIUM", + "confidence": "MEDIUM", + "category": "iac-ci", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "5.5", + "blastRadius": null, + "locationHint": "Dockerfile" + } + }, + { + "ruleId": "websec/iac", + "level": "note", + "message": { + "text": "docker-no-healthcheck: no HEALTHCHECK defined\n\nno HEALTHCHECK defined\n\nRemediation: Apply the hardening (non-root user, pin actions to a SHA, enforce TLS, etc.).\n\nCalibrated P(real)=0.569 (basis=label, n=51)." + }, + "partialFingerprints": { + "websecFingerprintV1": "fc886e46ce90c502" + }, + "properties": { + "severity": "LOW", + "confidence": "MEDIUM", + "category": "iac-ci", + "calibrated": { + "p": 0.569, + "ci": [ + 0.433, + 0.695 + ], + "n": 51, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "3.0", + "blastRadius": null, + "locationHint": "Dockerfile" + } + }, + { + "ruleId": "websec/incomplete-hsts", + "level": "note", + "message": { + "text": "no-hsts: browser/transport hardening header\n\nNo Strict-Transport-Security header found. Apply HSTS at the edge to ALL responses (`max-age>=31536000; includeSubDomains; preload` where the domain model allows) so the first-load page can't be downgraded over plaintext.\n\nRemediation: Apply HSTS uniformly at the EDGE to ALL responses (not just /api): `max-age>=31536000; includeSubDomains; preload` where the domain model allows.\n\nCalibrated P(real)=0.125 (basis=label, n=8)." + }, + "partialFingerprints": { + "websecFingerprintV1": "339f3c8ca3588ced" + }, + "properties": { + "severity": "LOW", + "confidence": "LOW", + "category": "transport", + "calibrated": { + "p": 0.125, + "ci": [ + 0.022, + 0.471 + ], + "n": 8, + "basis": "label", + "note": "indicative \u2014 calibrated on a deliberately-vulnerable app corpus; skews optimistic on clean production code" + }, + "security-severity": "3.0", + "blastRadius": null, + "locationHint": "(response headers)" + } + } + ] + } + ] +} \ No newline at end of file