clauditDashboardclaude-opus-5

Security

Security & Privacy · 7/27/2026, 7:19:26 PM · 171 chars

This is a single Next.js App Router GET route handler that looks up a user by an `id` query parameter. It interpolates that attacker-controlled parameter directly into an SQL string, performs no authentication or authorization, and returns every column of the matching row(s) verbatim. As written it is an unauthenticated SQL injection and full user-table disclosure primitive and must not ship.

Access Control 1/10Injection 1/10Cryptography 5/10Configuration 3/10Dependencies 5/10Composite 1/10

Score over time · 5 runs of this auditor

8 findingsfirst run of this auditor

  1. criticalcertain · vulnerabilityOWASP Top 10 (2021) Coverage

    SQL injection via string-interpolated query parameter

    GET handler, line 1: db.query(`SELECT * FROM users WHERE id=${id}`)

    The `id` value is read straight from `new URL(req.url).searchParams.get("id")` and template-interpolated into the SQL text. There is no parameter binding, no type coercion, no allow-list and no escaping, so the caller controls SQL syntax, not just a value. This is CWE-89 / OWASP A03. Depending on the driver, stacked statements (`;`) may also be available, extending the impact from read to write/DDL.

    Fails when: GET /api/route?id=1%20OR%201=1 produces `SELECT * FROM users WHERE id=1 OR 1=1` and returns the entire users table as JSON. GET /api/route?id=1%20UNION%20SELECT%20table_name,null,null%20FROM%20information_schema.tables lets the attacker pivot to arbitrary schema and data exfiltration; with a multi-statement-capable driver, `?id=1;DROP TABLE users--` destroys data.

    Fix: Use a parameterized query and validate the type first, e.g. `const id = Number(new URL(req.url).searchParams.get("id")); if (!Number.isInteger(id) || id <= 0) return new Response(null,{status:400}); const rows = await db.query('SELECT id, email, name FROM users WHERE id = $1', [id]);` Never build SQL with template literals from request data.

  2. criticalcertain · vulnerabilityAuthentication & Authorization Analysis

    Endpoint performs no authentication before returning user records

    GET handler — entire body, line 1 (no session/token check before db.query)

    The handler never inspects cookies, an Authorization header, or any session helper. Any anonymous internet client that can reach the route can read user rows. This is OWASP A01/A07 (CWE-306, missing authentication for critical function) and it is what turns the injection above from an authenticated bug into a pre-auth compromise.

    Fails when: An unauthenticated attacker issues `curl https://app/api/route?id=1` and receives the full user record for user 1, including any columns the table holds. Combined with the injection, one unauthenticated request dumps the table.

    Fix: Resolve and verify the caller before touching the database: `const session = await auth(); if (!session) return new Response(null,{status:401});` Enforce this in middleware for the whole route segment so new handlers inherit it.

  3. highcertain · vulnerabilityAuthentication & Authorization Analysis

    Insecure direct object reference — caller-supplied id is used with no ownership check

    GET handler — `searchParams.get("id")` used directly as the row selector

    Even with authentication added, the record returned is chosen entirely by a client-supplied identifier with no comparison against the authenticated principal and no role check. This is a textbook IDOR / broken object-level authorization (CWE-639, OWASP A01).

    Fails when: Logged-in user 42 requests `?id=43` and receives another customer's record; iterating id=1..N enumerates every user in the system.

    Fix: Either derive the identifier from the session (`session.user.id`) and ignore the query parameter, or scope the query and authorize explicitly: `WHERE id = $1 AND (id = $2 OR $3 = true)` with the session id and an admin flag, returning 403/404 when the check fails.

  4. highcertain · vulnerabilityHardcoded Secrets & Sensitive Data Exposure

    SELECT * returns all user columns, including credential and PII fields, unfiltered to the client

    GET handler: `SELECT * FROM users` and `return Response.json(rows)`

    The query selects every column and the result set is serialized directly into the HTTP response with no DTO, field allow-list, or redaction. A `users` table typically carries password_hash, mfa_secret, password_reset_token, api_key, email, phone and address columns; all of them are shipped to the caller. This is OWASP A02/A04 (CWE-200 exposure of sensitive information).

    Fails when: `GET /api/route?id=1` returns `{"password_hash":"$2b$...","reset_token":"...","mfa_secret":"...","email":"..."}`. The attacker takes the hashes offline for cracking and uses a live reset_token to take over the account without ever knowing the password.

    Fix: Select and return an explicit allow-list only, and map to a response DTO: `SELECT id, name, avatar_url FROM users WHERE id = $1`, then `return Response.json({ id: row.id, name: row.name, avatarUrl: row.avatar_url })`.

  5. mediumcertain · vulnerabilityInput Validation & Output Encoding

    Query parameter is neither present-checked nor type-validated

    GET handler: `const id = new URL(req.url).searchParams.get("id")`

    `searchParams.get` returns `string | null`. There is no check for absence, no numeric/UUID validation, and no length bound. The value flows straight into the data layer, so malformed and missing input reach the database instead of being rejected at the boundary with a 400.

    Fails when: A request with no parameter at all (`GET /api/route`) yields `id === null`, producing `SELECT * FROM users WHERE id=null` — on some engines a syntax/type error and a 500, on others a silent empty result; a very long value (`?id=` + 1MB of digits) is passed unbounded to the DB, wasting a connection and enabling cheap resource abuse.

    Fix: Validate at the edge with a schema, e.g. `const parsed = z.coerce.number().int().positive().safeParse(searchParams.get("id")); if (!parsed.success) return Response.json({error:"invalid id"},{status:400});` and use `parsed.data` thereafter.

  6. mediumcertain · deficiencyInsecure Design

    No rate limiting on a record-lookup endpoint

    GET handler — no throttling, quota, or abuse control anywhere in the route

    The route accepts unlimited requests per client. For an endpoint that resolves an incrementing identifier to a user record, absence of throttling makes bulk enumeration and injection fuzzing free and fast, and leaves the database connection pool open to exhaustion (OWASP A04, CWE-770).

    Fails when: An attacker scripts 100k requests/minute cycling `?id=1..100000` (or blind-injection payloads); the entire user base is scraped in minutes and the DB pool saturates, causing 5xx errors for legitimate traffic.

    Fix: Apply a per-IP and per-principal limiter in middleware (e.g. Upstash Ratelimit / `@vercel/firewall`) returning 429 with `Retry-After`, and cap concurrent DB work with a bounded pool and query timeout.

  7. mediumlikely · deficiencySecurity Misconfiguration

    No error handling around the database call; driver errors propagate to the response

    GET handler: `const rows = await db.query(...)` with no try/catch

    The awaited call is unguarded, so any driver error (syntax error from injected input, connection failure, constraint error) rejects out of the handler. Framework error pages and logs frequently echo the driver message — which contains the failing SQL text and schema identifiers — and in non-production builds the stack trace as well. This gives an attacker a verbose error oracle for blind injection (CWE-209).

    Fails when: `GET /api/route?id=1'` makes the driver throw `syntax error at or near "'" ... SELECT * FROM users WHERE id=1'`; the attacker reads the reflected message, confirms the injection point and the backend engine, and tunes payloads from the error text alone.

    Fix: Wrap the call: `try { ... } catch (e) { logger.error(e); return Response.json({error:"internal error"},{status:500}); }` — log server-side with a correlation id, never return driver messages, and ensure production builds disable detailed error output.

  8. lowlikely · deficiencySecurity Misconfiguration

    Sensitive JSON response sent without no-store cache directives

    GET handler: `return Response.json(rows)` — no headers argument

    `Response.json` sets only `content-type`. A GET response carrying per-user data with no `Cache-Control: no-store, private` and no `Vary: Authorization/Cookie` may be retained by shared proxies, CDNs, or the browser disk cache and served to the wrong principal (CWE-525).

    Fails when: A CDN or corporate proxy fronting the app caches the response for `?id=7`; a different user requesting the same URL is served user 7's record from cache, and the data also persists in the browser cache on a shared kiosk after logout.

    Fix: `return Response.json(body, { headers: { "Cache-Control": "no-store, private, max-age=0", "Vary": "Cookie, Authorization" } })` and set `export const dynamic = "force-dynamic"` for the route.