Security
Security & Privacy · 7/28/2026, 3:27:10 AM · 122 chars
The submission is a single Next.js App Router GET route handler that interpolates an unvalidated query-string parameter directly into a SQL string and returns the raw driver result instead of a Response. It is trivially exploitable for full database read/write via SQL injection, has no authentication, validation, or error handling, and is unfit to deploy in any form. Every line of the handler contains a defect.
Score over time · 5 runs of this auditor
8 findings2 new since the previous run
- lowcertain · deficiencyDependency & Supply Chain Risknew
`db` is referenced as an unimported free identifier
GET handler — `db.query(...)` with no corresponding `import` statement in the module
The module uses `db` without importing or otherwise binding it. Either it is a global injected at runtime, or the import was omitted from the submission. A global mutable database handle has no type safety, cannot be swapped for a test double, and makes it impossible to audit which client, pool configuration, or TLS settings are in effect — including whether the connection uses a least-privilege role.
Fails when: At runtime `db` is undefined, so the first request throws `ReferenceError: db is not defined` and the route 500s; alternatively a globally shared handle carries a superuser role, so the SQL injection above escalates from table read to full database administration.
Fix: Import an explicitly configured client from a single module, e.g. `import { db } from '@/lib/db'`, and configure that pool with a least-privilege role, TLS, and a statement timeout.
- criticalcertain · vulnerabilityOWASP Top 10 (2021) Coverage
SQL injection via template-literal interpolation of the `q` query parameter
GET handler — db.query(`SELECT * FROM t WHERE n=${q}`)
`q` is taken verbatim from `new URL(r.url).searchParams.get("q")` and spliced into a SQL template literal with no parameterisation, escaping, allow-listing, or type coercion. The attacker fully controls the tail of the WHERE clause. Depending on the driver's multi-statement setting, this is either arbitrary read of any table or arbitrary statement execution. This is CWE-89 / OWASP A03.
Fails when: Request `GET /route?q=1 OR 1=1` returns every row of table `t`. `GET /route?q=1 UNION SELECT username,password_hash,1,1 FROM users--` exfiltrates credentials. If the driver permits stacked statements, `GET /route?q=1; DROP TABLE t--` destroys the table. Blind/time-based extraction (`q=1 AND pg_sleep(5)`) works even if the response body is suppressed.
Fix: Use a parameterised query and never interpolate: `const rows = await db.query('SELECT n, ... FROM t WHERE n = $1', [n])` (or the driver's `?` placeholder form). Coerce and validate first, e.g. `const n = Number(q); if (!Number.isInteger(n)) return new Response('bad request', {status:400});`
- highcertain · vulnerabilityInput Validation & Output Encodingnew
Route handler returns the database driver result instead of a Response object
GET handler — `return db.query(...)`
A Next.js App Router route handler must resolve to a `Response`/`NextResponse`. Here the handler returns the promise from `db.query`, whose resolved value is a driver-specific result object (e.g. pg `Result` with `rows`, `fields`, `command`, or a mysql2 `[rows, fields]` tuple). Next will not treat this as a Response; the framework either throws at serialisation time or coerces the object in an unspecified way. No status code, no `Content-Type`, and no `Cache-Control` are ever set.
Fails when: A well-formed `GET /route?q=1` produces a runtime error inside the framework's response handling (or an untyped body served without `Content-Type`), so even the happy path is broken. If the framework does serialise the driver object, internal column metadata from `fields` (table OIDs, type OIDs, real column names) is leaked to the client.
Fix: Await the query, project explicit fields, and wrap: `const { rows } = await db.query(sql, params); return Response.json(rows.map(pick), { headers: { 'Cache-Control': 'no-store' } });`
- highcertain · vulnerabilityInput Validation & Output Encoding
`q` is used without null-check, type check, or bounds check
GET handler — `const q = new URL(r.url).searchParams.get("q")`
`URLSearchParams.get` returns `null` when the parameter is absent, and an arbitrary-length string otherwise. Neither case is handled: there is no presence check, no type coercion, no length cap, and no allow-list. The value flows unmodified into the query builder. This is the root enabler of the injection above and an independent correctness defect.
Fails when: `GET /route` with no query string yields `q === null`, producing the SQL text `SELECT * FROM t WHERE n=null` — a silently empty result set rather than a 400, masking client errors. A multi-megabyte `q` value is also accepted and sent to the database, consuming connection and parser resources.
Fix: Validate before use, ideally with a schema: `const parsed = z.coerce.number().int().safeParse(q); if (!parsed.success) return new Response('bad request', {status:400});` and pass `parsed.data` as a bound parameter.
- highlikely · vulnerabilityAuthentication & Authorization Analysis
Endpoint performs a database read with no authentication or authorization check
GET handler — entire function body, no session/token/role check before `db.query`
The handler moves straight from parsing the URL to querying the database. There is no call to a session helper, no bearer-token verification, no role or tenant check, and no ownership filter on the row selected by `n`. Any anonymous internet client that can reach the route can read table `t`. No middleware was supplied, so an external gate cannot be confirmed; on the submitted code the endpoint is unauthenticated.
Fails when: An unauthenticated attacker issues `GET /route?q=1` and receives rows from table `t` belonging to arbitrary users or tenants, without ever holding a credential.
Fix: Resolve the caller's identity first and reject early: `const session = await auth(); if (!session) return new Response('unauthorized', {status:401});` then scope the query to the caller, e.g. `WHERE n = $1 AND owner_id = $2`.
- mediumcertain · vulnerabilityOWASP Top 10 (2021) Coverage
`SELECT *` returns every column of the table to the client
GET handler — `SELECT * FROM t`
The query selects all columns and the result is handed straight back to the caller with no projection, field allow-list, or DTO mapping. Any column added to table `t` later — password hashes, tokens, internal flags, PII, soft-delete markers — is automatically published to every caller with no code change and no review trigger. This is OWASP A01/API3 excessive data exposure.
Fails when: A migration adds `email` and `reset_token` columns to `t`. Without touching this file, `GET /route?q=1` begins returning both values to any caller, silently turning a benign lookup endpoint into a credential-reset oracle.
Fix: Enumerate the columns the endpoint is contracted to return: `SELECT id, name FROM t WHERE n = $1`, and map to an explicit response DTO before serialising.
- mediumcertain · vulnerabilityOWASP Top 10 (2021) Coverage
No try/catch around the database call; driver errors propagate unhandled
GET handler — `return db.query(...)` with no try/catch and no `.catch`
The promise returned by `db.query` is returned rather than awaited in a guarded block. Any rejection — syntax error from injected input, connection pool exhaustion, permission denial — escapes the handler entirely. Nothing logs it with request context, and the framework's default error path may surface driver detail. This is CWE-248 combined with CWE-209 (error message information leak).
Fails when: Attacker sends `GET /route?q='` producing malformed SQL. The driver rejects with a message such as `unterminated quoted string at or near "'" ... SELECT * FROM t WHERE n='`, which the default error boundary can echo to the client, confirming injectability and disclosing the table name and query shape. The rejection is also never recorded in application logs with the offending input.
Fix: Wrap in try/catch, log server-side with a correlation id, and return a generic body: `try { ... } catch (e) { logger.error({ err: e, reqId }); return new Response('internal error', { status: 500 }); }`
- mediumlikely · deficiencyAuthentication & Authorization Analysis
Unauthenticated database-backed endpoint has no rate limiting
GET handler — no throttle, quota, or cost guard before `db.query`
Every request opens a database round trip with no per-IP or per-key limiter, no query timeout, and no result-set cap (`SELECT *` with no LIMIT). Combined with the injection above, this makes automated blind extraction and connection-pool exhaustion cheap. No limiter middleware was supplied with the submission.
Fails when: An attacker scripts 10k requests/second of `GET /route?q=1 AND pg_sleep(10)--`; each holds a pooled connection for ten seconds, exhausting the pool and taking the whole application offline while also enabling time-based data extraction.
Fix: Apply a per-IP/per-token limiter (e.g. Upstash Ratelimit or a middleware bucket) ahead of the handler, set a statement timeout on the pool, and add an explicit `LIMIT` to the query.