nextjs-server-security-review
Statically review Next.js middleware, Server Actions, next.config.js, and environment-variable files for four documented server-side defect classes -- middleware matcher exclusions that silently skip auth on Server Functions, Server Actions missing allowedOrigins CSRF protection,
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/nextjs-server-security-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Next.js Server Security Review
Purpose
Review Next.js middleware (middleware.ts/.js), Server Actions, next.config.js, and environment-variable files for the concrete server-side defect classes Next.js's own documentation calls out directly: middleware matcher exclusions that silently skip authorization for Server Functions, Server Actions missing allowedOrigins CSRF protection, secrets accidentally exposed to the client via a NEXT_PUBLIC_ prefix, and server-side request forgery / open-redirect risk via dangerouslyAllowLocalIP or an unvalidated rewrite destination. This skill exists so the review stays anchored to these documented defect classes instead of drifting into a general "Next.js code review" of component architecture, data fetching patterns, or styling.
When to use
Use this skill when the user asks to:
- review
middleware.ts/middleware.jsand itsmatcherconfiguration for authorization gaps, - assess whether a Server Action is protected against CSRF, or whether
next.config.js'sserverActions.allowedOriginsis configured correctly for a reverse-proxy or multi-zone deployment, - audit
.env/.env.productionfiles or anyprocess.env.NEXT_PUBLIC_*usage for accidental secret exposure to the client bundle, - review
next.config.jsimage configuration (images.dangerouslyAllowLocalIP) orrewrites()/NextResponse.rewrite()usage for SSRF or open-redirect risk, - perform a pre-launch security review of a Next.js server surface (middleware, Server Actions, config, env files).
Do not use this skill for:
- client-only React component logic with no middleware, Server Action, config, or environment-variable surface in scope — there is no server-side sink for this skill to review,
- general Next.js performance, data-fetching-pattern, or App Router/Pages Router migration review with no security angle,
- a bug that requires live traffic reproduction (actually triggering a CSRF request cross-origin, actually confirming an SSRF callback from a deployed image optimizer) to prove exploitation — static analysis proves the structural risk, not that it has already been exploited in production.
Context7 Documentation Protocol
- Resolve the Next.js library ID with
resolve-library-id(matched result:/vercel/next.js) before citing any middleware, Server Actions, environment-variable, ornext.config.jsbehavior claim. /vercel/next.jsis a high-reputation source covering authentication, middleware, Server Actions, environment variables, andnext.config.jssecurity patterns directly from the framework's own docs and source. Usequery-docsagainst it to confirm exact option names (serverActions.allowedOrigins,images.dangerouslyAllowLocalIP) and documented behavior (NEXT_PUBLIC_inlining, matcher exclusion semantics) before writing a finding.- Read
package.jsonfirst to confirm the Next.js major version and whether the app uses the App Router or Pages Router —experimental.serverActionsconfiguration, middlewarematcherconventions, andNextResponse.rewrite()APIs have shifted across major versions; do not apply App Router API names to a Pages Router codebase or vice versa. - If Context7 is unavailable, fall back to the
official_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-based, unverified against current release.
Lean operating rules
- All four defect classes in this skill's scope default to HIGH severity. This is a security-scoped skill: do not downgrade a middleware-matcher authorization gap, a missing CSRF allowlist, a leaked secret, or a structural SSRF/open-redirect path to MEDIUM just because it has not been observed exploited yet — the risk is in the structure, not in whether someone has already hit it.
- Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this middleware might not protect everything" or "this env var might leak" without showing the specific
matcherpattern, the specificserverActionsconfig, or the specific variable declaration is not a valid finding — it is a guess. - Never accept a middleware
matcheras sufficient authorization coverage in isolation. Amatcherwhose negative-lookahead pattern excludes a path (e.g./((?!api|_next).*)) means middleware — and any auth check it performs — never runs for that excluded path; a Proxy matcher that excludes a path also skips Server Function calls on that path. Confirm the excluded path's own handlers (Server Actions, Route Handlers) independently verify the session themselves. - Never approve a
next.config.jswith aserverActionsblock configured for a reverse-proxy, multi-zone, or otherwise cross-origin deployment unlessallowedOriginsis explicitly present in that same block. Next.js's default CSRF protection compares the Server Action request'sOriginheader to theHostheader and rejects mismatches — a deployment where those two headers legitimately differ (reverse proxy, multi-zone) needs the allowlist or every legitimate request fails, or worse, the mismatch is worked around insecurely elsewhere. - Flag every environment variable whose name suggests a secret (contains
KEY,SECRET,TOKEN,PASSWORD,CREDENTIAL, or equivalent) and is prefixedNEXT_PUBLIC_. AnyNEXT_PUBLIC_variable is inlined into the JavaScript bundle at build time and shipped to every client, full stop — there is no runtime gate that can retroactively hide it once bundled. The safe fix is to drop the prefix and read the value only from server-side code viaprocess.env.API_KEY(or the equivalent unprefixed name), never from a Client Component. - Flag
images.dangerouslyAllowLocalIP: trueinnext.config.jsas a structural SSRF risk unless the codebase demonstrably validates every dynamicsrcvalue against a hardcoded external-hostname allowlist before it reaches the<Image>component. - Flag any
NextResponse.rewrite()call (orrewrites()destination) whose target URL is built directly from user-controlled input (query parameters, headers, request body) with no hostname allowlist check on the resolved value before the rewrite call — this is SSRF/open-redirect via the rewrite backend, not a cosmetic routing bug. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only).
- Load only the reference needed for the concern in scope.
References
Load these only when needed:
- Review workflow and findings contract — use for the step-by-step review procedure, the decision tree per defect class, and the required output shape.
- Middleware and Server Actions boundary defects — load only when reviewing
middleware.ts/.js, itsmatcher, or Server Action CSRF configuration. - Environment variables and SSRF/redirect surfaces — load only when the review scope includes
.env*files,NEXT_PUBLIC_*usage,images.dangerouslyAllowLocalIP, or a rewrite/redirect destination built from dynamic input. Includes the OWASP SSRF/open-redirect grounding reference; load that citation only when such a finding is actually present.
Response minimum
Return, at minimum:
- the middleware file(s), Server Action(s),
next.config.js, and/or environment file(s) in scope, - ranked findings with file:line evidence, defect category (
middleware-auth-gap,csrf-origin,secret-leak, orssrf-redirect), the concrete data-flow trace (thematcherpattern and the excluded path's own auth handling, theserverActionsconfig and its deployment topology, the environment variable declaration and its usage site, or the origin-to-sink path for the SSRF/redirect finding), and a fix sketch matching Next.js's documented pattern, - for every middleware-auth-gap finding, an explicit statement of whether the excluded path's own handler independently verifies the session — never approve on the assumption it does,
- evidence level per finding (
repo evidence,documentation-based, orinference), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block),
- open questions or scope the review could not cover (e.g., "confirming actual cross-origin CSRF success requires a live cross-origin request, not static review").
Files (vanguard-frontier-agentic)
-
references
-
env-and-ssrf-surfaces.md 6.8 KB
# Environment Variables, Image SSRF, and Rewrite/Redirect Injection Use this reference only when the review scope includes `.env*` files, `NEXT_PUBLIC_*` usage, `images.dangerouslyAllowLocalIP` in `next.config.js`, or a rewrite/redirect destination built from dynamic input. The OWASP SSRF/open-redirect citation is loaded only when such a finding is actually present — do not cite it preemptively in a review with no SSRF/redirect surface. ## What people get wrong: `NEXT_PUBLIC_` secrets The naive assumption is: > "I'll just prefix it with `NEXT_PUBLIC_` so the client component I'm writing right now can read it — I'll clean it up later." Wrong, and unrecoverable after the fact. Next.js's own documentation is direct: environment variables prefixed `NEXT_PUBLIC_` are inlined into the JavaScript bundle at build time (`documentation-based`, Next.js environment-variables guide). This is not a runtime gate that can be toggled off later — every client that has ever loaded a build containing the value has received it, in plaintext, in a bundle they can inspect via browser devtools or by fetching the JS file directly. Rotating the underlying secret afterward does not undo the exposure of the old value's existence, format, or any access it already granted before rotation. ## Officially grounded rule Non-`NEXT_PUBLIC_` environment variables are exclusively available in the Node.js server environment and are never sent to the browser (`documentation-based`, Next.js environment-variables guide). The correct pattern is: ```txt # .env API_KEY=<REDACTED_secret_value> ``` ```js // Server Action or Route Handler only — never a Client Component export async function myServerAction() { const key = process.env.API_KEY // ... } ``` ## Non-negotiable design rules ### 1. Flag by name pattern, then confirm by value shape Any `NEXT_PUBLIC_`-prefixed variable whose name contains `KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `CREDENTIAL`, or an obvious equivalent is a finding regardless of the value shown in the file — even a placeholder or rotated value in a committed `.env.example` demonstrates the naming pattern will leak the real value wherever it is actually set. ### 2. A genuinely public value under `NEXT_PUBLIC_` is not a finding An analytics ID, a public feature-flag name, or a publishable (not secret) API key that the vendor itself documents as safe for client exposure (e.g., a Stripe *publishable* key, as opposed to a *secret* key) is the correct, intended use of the prefix. Do not manufacture a finding for a variable that is genuinely meant to be public — check the vendor's own documentation for whether a given key type is designed for client exposure before flagging it. ## What people get wrong: image optimization SSRF The naive assumption is: > "`dangerouslyAllowLocalIP` just lets me test with a local dev image server, it's not a real security control." The name is the warning. Enabling `dangerouslyAllowLocalIP` allows the built-in image optimizer to fetch a `src` URL that resolves to a loopback or internal-network address. Combined with any dynamic (user- or attacker-influenced) `src` value, this becomes a classic SSRF primitive: an attacker supplies a URL pointing at an internal service (a cloud metadata endpoint, an internal admin panel, a database's HTTP interface) and the server-side image optimizer fetches it on the attacker's behalf (`documentation-based`, Next.js image-configuration guide — generally not recommended due to potential SSRF risk). ## Non-negotiable design rule `images.dangerouslyAllowLocalIP: true` is a finding unless every dynamic `src` value reaching `<Image>` is validated against a hardcoded allowlist of safe external hostnames before it is used. `remotePatterns`/`domains` configuration that is itself permissive (a wildcard hostname pattern) does not clear this finding — the two controls are independent; a broad `remotePatterns` plus `dangerouslyAllowLocalIP: true` is worse, not better. ## What people get wrong: rewrite/redirect destination injection The naive assumption is: > "`NextResponse.rewrite()`/a dynamic rewrite is just routing logic, not a security-sensitive sink." Wrong when the destination is built from user-controlled input. `NextResponse.rewrite(new URL(userControlledValue, request.url))` (or an equivalent `rewrites()` destination built from request data) lets an attacker supply a URL that the rewrite forwards the request to — either an internal service (SSRF) or an external attacker-controlled host (functionally an open redirect, since the response the client ultimately sees originates from the rewritten destination). ## Non-negotiable design rule Any rewrite/redirect destination whose value traces back to user-controlled input (a query parameter, a header, a request body field) must pass through a hardcoded hostname allowlist check *before* the rewrite/redirect call — not merely be parsed with `new URL()` (which validates syntax, not safety) and passed straight through. Resolve the value to a local variable, check it against the allowlist, and only then call `NextResponse.rewrite()`/return the redirect. A destination built entirely from fixed, developer-authored literals is not a finding. ## OWASP grounding (load only when an SSRF/redirect finding is present) Server-Side Request Forgery is the vulnerability class both `dangerouslyAllowLocalIP` misuse and unvalidated rewrite destinations produce: server-side code is induced to make a request to a location the attacker chose rather than the application developer, potentially reaching internal-network resources otherwise unreachable from the public internet. The OWASP SSRF reference (listed in this skill's `official_docs`) provides the vendor-neutral grounding for why this defect class is treated as HIGH severity by default — it commonly enables internal network reconnaissance, cloud metadata credential theft, or bypass of network-perimeter controls, not merely a routing inconvenience. Cite it only in the specific finding write-up for a confirmed or suspected SSRF/redirect defect, not as boilerplate in every review. ## Verification targets - Grep every `.env*` file and any code referencing `process.env.NEXT_PUBLIC_` for a secret-shaped variable name. - Grep `next.config.js` for `dangerouslyAllowLocalIP` and check its value; if `true`, grep the codebase for every dynamic `<Image src=` binding and trace its origin. - Grep for `NextResponse.rewrite(` and `rewrites()` array entries; for each, trace the `destination`/URL argument's origin backward through variables to its source (literal, env var, or user-controlled request data). - Grep for a hostname allowlist check (`.includes(`, `ALLOWED_HOSTS`, or an equivalent project-specific allowlist array) and confirm its call site sits between the user-controlled origin and the rewrite/redirect call, not merely present elsewhere in the file. -
middleware-and-server-actions.md 5.4 KB
# Middleware Matcher Exclusions and Server Actions CSRF Use this reference only when reviewing `middleware.ts`/`.js`, its `matcher` configuration, or a Server Action's CSRF posture in `next.config.js`. ## What people get wrong: matcher exclusions The naive assumption is: > "Middleware runs before every request, so anything I check in middleware protects the whole app." Wrong, for any path the `matcher` excludes. A `matcher` written as a negative lookahead — e.g. `'/((?!api|_next).*)'` — is not just "skip static assets," it is "skip every path matching that lookahead, including API routes hosting Server Actions and Route Handlers." Next.js's own documentation on proxy execution order is explicit and non-negotiable on this point: a Proxy matcher that excludes a path also skips Server Function calls on that path (`documentation-based`, Next.js proxy/middleware docs). If the only auth check in the codebase lives inside middleware, and the matcher excludes `/api`, then every Server Action and Route Handler under `/api` runs with **zero** authorization enforcement from middleware — because middleware never runs for them at all. ## Non-negotiable design rules ### 1. A matcher exclusion is not evidence of anything — it is the absence of evidence Do not treat "middleware exists in this project" as sufficient. For each path the `matcher` excludes, that path's *own* handler must independently verify the session. There is no such thing as partial credit for "the matcher probably wasn't meant to exclude sensitive routes" — read the actual regex and the actual excluded paths. ### 2. Distinguish an explicit allowlist matcher from an exclusion-style matcher A `matcher` that explicitly lists the paths middleware should run on (e.g. `['/dashboard/:path*', '/admin/:path*']`) is a fundamentally different risk shape than a negative-lookahead exclusion matcher (e.g. `['/((?!api|_next).*)']`). The explicit-allowlist form makes it obvious which paths get middleware coverage and, by construction, does not accidentally exclude an API route someone forgot about. Recommend this form when a matcher-exclusion gap is found. ### 3. Trace what each Server Action actually checks For any Server Action (`'use server'` function) reachable from an excluded path, read its full body. A session check must be visible inside that function — not merely assumed because "there's an auth library imported at the top of the file." An import with no corresponding call site inside the action is not a check. ## What people get wrong: Server Actions CSRF The naive assumption is: > "Next.js handles CSRF for Server Actions automatically, so I don't need to configure anything." Half right. Next.js's built-in protection only allows `POST` requests and compares the request's `Origin` header to its `Host` header, aborting on mismatch (`documentation-based`, Next.js data-security guide). That default works for a simple same-origin deployment. It silently breaks down — or gets worked around insecurely — for any deployment where `Origin` and `Host` legitimately differ for real production traffic: an app behind a reverse proxy, a multi-zone setup where one domain fronts several Next.js apps, or a staging environment fronted by a different domain than the app's own `Host`. ## Officially grounded rule `serverActions.allowedOrigins` in `next.config.js` lets the framework compare the Server Action request's origin against an explicit, safe allowlist instead of only the automatic `Host`-header comparison: ```js /** @type {import('next').NextConfig} */ module.exports = { experimental: { serverActions: { allowedOrigins: ['my-proxy.com', '*.my-proxy.com'], }, }, } ``` (`documentation-based`, Next.js data-security and multi-zone guides.) ## Non-negotiable design rules ### 1. A `serverActions` block missing `allowedOrigins` is not automatically safe If the deployment is same-origin only (no reverse proxy, no multi-zone, `Origin` and `Host` always match for real traffic), omitting `allowedOrigins` is the documented safe default — do not manufacture a finding where none exists. But if the deployment topology is cross-origin (reverse proxy, multi-zone, CDN rewriting `Host`), a `serverActions` block that configures other options (`bodySizeLimit`, etc.) but omits `allowedOrigins` is the exact CSRF gap this rule exists to catch. Confirm the deployment topology before judging severity — check `next.config.js` `rewrites()`, deployment docs, or infrastructure config for reverse-proxy/multi-zone evidence. ### 2. `allowedOrigins` entries must be the actual safe origins, not a wildcard-everything pattern `allowedOrigins: ['*']` (or an equivalent catch-all) defeats the purpose of an allowlist — it accepts CSRF requests from any origin. Flag a wildcard-everything entry the same as a missing `allowedOrigins` key. ## Verification targets - Grep `middleware.ts`/`.js` for `matcher:` and inspect the pattern for a negative lookahead (`(?!...)`) versus an explicit path list. - Grep every file the matcher excludes for `'use server'` and read each exported action's full body for a session/auth check. - Grep `next.config.js` for `serverActions:` and check whether `allowedOrigins` is a sibling key inside that same object. - Grep deployment config / `rewrites()` / infra docs for reverse-proxy or multi-zone evidence to determine whether a missing `allowedOrigins` is actually a gap or a legitimate same-origin default. -
workflow-and-output.md 6.6 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure and the required output shape. Load the other two references only for the specific defect class the middleware, Server Action, config, or environment file under review actually raises. ## Prerequisites - Read `package.json` to confirm the Next.js major version and whether the app uses the App Router or Pages Router. `experimental.serverActions` configuration keys, middleware conventions, and rewrite/redirect APIs differ across major versions and routing modes; do not apply App Router API names to a Pages Router codebase or vice versa. - Locate every candidate file in scope: `middleware.ts`/`middleware.js` at the project root, `next.config.js`/`next.config.mjs`, any `.env*` file, and any file containing a `'use server'` directive or exported Server Action. ## Workflow 1. **Locate `middleware.ts`/`.js` and read its exported `config.matcher`.** Determine whether the `matcher` pattern excludes any path via a negative lookahead (e.g. `(?!api|_next)`). See `references/middleware-and-server-actions.md` for the decision tree. 2. **For every path the matcher excludes, locate that path's own handlers** (Route Handlers, Server Actions) and confirm each one independently verifies the session/auth state inside its own function body — never assume middleware covers it. 3. **Locate `next.config.js` and read any `experimental.serverActions` block.** If the deployment topology involves a reverse proxy or multi-zone setup (check deployment docs, `rewrites()`/proxy config, or ask if unclear), confirm `allowedOrigins` is present in that block and lists the actual production origins. 4. **Grep every `.env*` file and every `process.env.NEXT_PUBLIC_*` usage** for a variable name suggesting a secret (`KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `CREDENTIAL`, or equivalent). See `references/env-and-ssrf-surfaces.md`. 5. **Grep `next.config.js` for `images.dangerouslyAllowLocalIP`.** If `true`, confirm dynamic `src` values passed to `<Image>` are validated against a hardcoded external-hostname allowlist somewhere on their data-flow path. 6. **Grep for `NextResponse.rewrite(` and `rewrites()` destinations.** For each, trace the destination value's origin backward. If it is built from user-controlled input (query parameters, headers, request body) with no hostname allowlist check before the rewrite call, this is an SSRF/open-redirect finding. 7. **Produce ranked findings** using the output contract below. ## Decision tree - Middleware `matcher` excludes a path via negative lookahead, and that path's own handler has no independent session check → **HIGH** finding, `middleware-auth-gap`. Cite the documented execution-order rule directly (`documentation-based`): a Proxy matcher that excludes a path also skips Server Function calls on that path. - Middleware `matcher` excludes a path, but that path's own handler (Server Action or Route Handler) demonstrably verifies the session itself → not a finding for that path; note it explicitly as reviewed-and-clear. - `serverActions` block exists, deployment is cross-origin (reverse proxy/multi-zone), and `allowedOrigins` is absent from that block → **HIGH** finding, `csrf-origin`. - `serverActions` block exists and includes `allowedOrigins` listing the actual production origins, or the deployment is same-origin only and the key is legitimately omitted → not a finding. - A `NEXT_PUBLIC_`-prefixed variable name matches a secret-shaped pattern (`KEY`/`SECRET`/`TOKEN`/`PASSWORD`/`CREDENTIAL`) → **HIGH** finding, `secret-leak`, regardless of whether the value has been rotated since — the bundle already shipped it to every prior client that loaded the page. - A `NEXT_PUBLIC_`-prefixed variable holds genuinely non-secret data (an analytics ID, a public feature flag) → not a finding. - `images.dangerouslyAllowLocalIP: true` with no demonstrable `src` allowlist validation → **HIGH** finding, `ssrf-redirect`. - `images.dangerouslyAllowLocalIP` is `false` or absent (the default) → not a finding. - `NextResponse.rewrite()`/rewrite destination built from user-controlled input with no hostname allowlist check before the call → **HIGH** finding, `ssrf-redirect`. - Rewrite destination is a fixed literal path, or a dynamic value is checked against a hardcoded allowlist before the rewrite call → not a finding. ## Output contract Every response from this skill must return: 1. **Scope** — the middleware file(s), Server Action(s), `next.config.js`, and/or environment file(s) reviewed. 2. **Ranked findings** — each with file:line, defect category (`middleware-auth-gap` / `csrf-origin` / `secret-leak` / `ssrf-redirect`), the concrete data-flow trace (matcher pattern and excluded-path handler status, serverActions config and deployment topology, environment variable declaration and usage site, or origin-to-sink path for SSRF/redirect), and a fix sketch matching Next.js's documented pattern. 3. **Middleware-auth-gap status per excluded path** — an explicit statement of whether the excluded path's own handler independently verifies the session; never infer one does. 4. **Evidence level per finding** — `repo evidence`, `documentation-based`, or `inference`. Label structural risk findings as structural risk explicitly — do not imply confirmed exploitation without live evidence (e.g., a captured cross-origin CSRF success, a confirmed SSRF callback). 5. **Verdict** — approve / approve-with-notes / block. 6. **Open questions or out-of-scope items** — e.g., "confirming actual cross-origin CSRF exploitation requires a live cross-origin request, not static review," or "component-level data-fetching patterns in this same file are out of scope — recommend a general Next.js review if needed." ## When to push back Push back if the user asks to: - approve a middleware `matcher` that excludes `/api` (or another path) as sufficient authorization coverage without checking whether that excluded path's own handlers verify the session — "middleware is there" is not evidence for a path middleware never runs on, - skip the `serverActions.allowedOrigins` check because "we haven't seen a CSRF incident" — this defect class is structural and often invisible until an attacker actually attempts a cross-origin form submission, - clear a `NEXT_PUBLIC_`-prefixed secret because "we're going to rotate it" — rotation does not undo exposure already shipped to every client that loaded a build containing the old value, and the review is about the structural leak, not the current key's live validity, - downgrade an untraced SSRF/rewrite finding to informational because "it's probably fine" — this skill's default is HIGH for exactly this class of unproven claim.
-
-
metadata.json 2 KB
{ "id": "nextjs-server-security-review", "name": "Next.js Server Security Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews Next.js middleware, Server Actions, next.config.js, and environment-variable files for middleware matcher exclusions that silently skip auth on Server Functions, Server Actions missing allowedOrigins CSRF protection, secrets leaked via NEXT_PUBLIC_ prefixes, and SSRF/open-redirect via dangerouslyAllowLocalIP or unvalidated rewrite destinations, grounding claims via Context7 and Next.js's own documentation.", "source_type": "original", "official_docs": [ "https://nextjs.org/docs/app/building-your-application/authentication", "https://nextjs.org/docs/app/guides/data-security", "https://nextjs.org/docs/app/guides/environment-variables", "https://nextjs.org/docs/app/api-reference/file-conventions/proxy", "https://owasp.org/www-community/attacks/Server_Side_Request_Forgery", "https://owasp.org/www-community/attacks/xss/" ], "security_notes": "This skill's entire scope is security-critical: a middleware matcher exclusion is a zero-trust boundary defect (authorization silently skipped on excluded paths), a missing serverActions.allowedOrigins is a CSRF gap, a NEXT_PUBLIC_-prefixed secret is a build-time data exposure with no runtime remediation once shipped, and dangerouslyAllowLocalIP or an unvalidated rewrite destination is a server-side request forgery / open-redirect vector. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete repo evidence. Static-review-only skill: it reads and greps middleware, Server Action, config, and environment files but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-03", "path": "skills/frontend/nextjs-server-security-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 8.9 KB
--- name: nextjs-server-security-review description: Statically review Next.js middleware, Server Actions, next.config.js, and environment-variable files for four documented server-side defect classes -- middleware matcher exclusions that silently skip auth on Server Functions, Server Actions missing allowedOrigins CSRF protection, secrets leaked via NEXT_PUBLIC_ prefixes, and SSRF/open-redirect via dangerouslyAllowLocalIP or unvalidated rewrite destinations. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-03" category: security --- # Next.js Server Security Review ## Purpose Review Next.js middleware (`middleware.ts`/`.js`), Server Actions, `next.config.js`, and environment-variable files for the concrete server-side defect classes Next.js's own documentation calls out directly: middleware `matcher` exclusions that silently skip authorization for Server Functions, Server Actions missing `allowedOrigins` CSRF protection, secrets accidentally exposed to the client via a `NEXT_PUBLIC_` prefix, and server-side request forgery / open-redirect risk via `dangerouslyAllowLocalIP` or an unvalidated rewrite destination. This skill exists so the review stays anchored to these documented defect classes instead of drifting into a general "Next.js code review" of component architecture, data fetching patterns, or styling. ## When to use Use this skill when the user asks to: - review `middleware.ts`/`middleware.js` and its `matcher` configuration for authorization gaps, - assess whether a Server Action is protected against CSRF, or whether `next.config.js`'s `serverActions.allowedOrigins` is configured correctly for a reverse-proxy or multi-zone deployment, - audit `.env`/`.env.production` files or any `process.env.NEXT_PUBLIC_*` usage for accidental secret exposure to the client bundle, - review `next.config.js` image configuration (`images.dangerouslyAllowLocalIP`) or `rewrites()`/`NextResponse.rewrite()` usage for SSRF or open-redirect risk, - perform a pre-launch security review of a Next.js server surface (middleware, Server Actions, config, env files). Do not use this skill for: - client-only React component logic with no middleware, Server Action, config, or environment-variable surface in scope — there is no server-side sink for this skill to review, - general Next.js performance, data-fetching-pattern, or App Router/Pages Router migration review with no security angle, - a bug that requires live traffic reproduction (actually triggering a CSRF request cross-origin, actually confirming an SSRF callback from a deployed image optimizer) to prove exploitation — static analysis proves the structural risk, not that it has already been exploited in production. ## Context7 Documentation Protocol - Resolve the Next.js library ID with `resolve-library-id` (matched result: `/vercel/next.js`) before citing any middleware, Server Actions, environment-variable, or `next.config.js` behavior claim. - `/vercel/next.js` is a high-reputation source covering authentication, middleware, Server Actions, environment variables, and `next.config.js` security patterns directly from the framework's own docs and source. Use `query-docs` against it to confirm exact option names (`serverActions.allowedOrigins`, `images.dangerouslyAllowLocalIP`) and documented behavior (`NEXT_PUBLIC_` inlining, matcher exclusion semantics) before writing a finding. - Read `package.json` first to confirm the Next.js major version and whether the app uses the App Router or Pages Router — `experimental.serverActions` configuration, middleware `matcher` conventions, and `NextResponse.rewrite()` APIs have shifted across major versions; do not apply App Router API names to a Pages Router codebase or vice versa. - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based, unverified against current release`. ## Lean operating rules - All four defect classes in this skill's scope default to HIGH severity. This is a security-scoped skill: do not downgrade a middleware-matcher authorization gap, a missing CSRF allowlist, a leaked secret, or a structural SSRF/open-redirect path to MEDIUM just because it has not been observed exploited yet — the risk is in the structure, not in whether someone has already hit it. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this middleware might not protect everything" or "this env var might leak" without showing the specific `matcher` pattern, the specific `serverActions` config, or the specific variable declaration is not a valid finding — it is a guess. - Never accept a middleware `matcher` as sufficient authorization coverage in isolation. A `matcher` whose negative-lookahead pattern excludes a path (e.g. `/((?!api|_next).*)`) means middleware — and any auth check it performs — never runs for that excluded path; a Proxy matcher that excludes a path also skips Server Function calls on that path. Confirm the excluded path's own handlers (Server Actions, Route Handlers) independently verify the session themselves. - Never approve a `next.config.js` with a `serverActions` block configured for a reverse-proxy, multi-zone, or otherwise cross-origin deployment unless `allowedOrigins` is explicitly present in that same block. Next.js's default CSRF protection compares the Server Action request's `Origin` header to the `Host` header and rejects mismatches — a deployment where those two headers legitimately differ (reverse proxy, multi-zone) needs the allowlist or every legitimate request fails, or worse, the mismatch is worked around insecurely elsewhere. - Flag every environment variable whose name suggests a secret (contains `KEY`, `SECRET`, `TOKEN`, `PASSWORD`, `CREDENTIAL`, or equivalent) and is prefixed `NEXT_PUBLIC_`. Any `NEXT_PUBLIC_` variable is inlined into the JavaScript bundle at build time and shipped to every client, full stop — there is no runtime gate that can retroactively hide it once bundled. The safe fix is to drop the prefix and read the value only from server-side code via `process.env.API_KEY` (or the equivalent unprefixed name), never from a Client Component. - Flag `images.dangerouslyAllowLocalIP: true` in `next.config.js` as a structural SSRF risk unless the codebase demonstrably validates every dynamic `src` value against a hardcoded external-hostname allowlist before it reaches the `<Image>` component. - Flag any `NextResponse.rewrite()` call (or `rewrites()` destination) whose target URL is built directly from user-controlled input (query parameters, headers, request body) with no hostname allowlist check on the resolved value before the rewrite call — this is SSRF/open-redirect via the rewrite backend, not a cosmetic routing bug. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only). - Load only the reference needed for the concern in scope. ## References Load these only when needed: - [Review workflow and findings contract](references/workflow-and-output.md) — use for the step-by-step review procedure, the decision tree per defect class, and the required output shape. - [Middleware and Server Actions boundary defects](references/middleware-and-server-actions.md) — load only when reviewing `middleware.ts`/`.js`, its `matcher`, or Server Action CSRF configuration. - [Environment variables and SSRF/redirect surfaces](references/env-and-ssrf-surfaces.md) — load only when the review scope includes `.env*` files, `NEXT_PUBLIC_*` usage, `images.dangerouslyAllowLocalIP`, or a rewrite/redirect destination built from dynamic input. Includes the OWASP SSRF/open-redirect grounding reference; load that citation only when such a finding is actually present. ## Response minimum Return, at minimum: - the middleware file(s), Server Action(s), `next.config.js`, and/or environment file(s) in scope, - ranked findings with file:line evidence, defect category (`middleware-auth-gap`, `csrf-origin`, `secret-leak`, or `ssrf-redirect`), the concrete data-flow trace (the `matcher` pattern and the excluded path's own auth handling, the `serverActions` config and its deployment topology, the environment variable declaration and its usage site, or the origin-to-sink path for the SSRF/redirect finding), and a fix sketch matching Next.js's documented pattern, - for every middleware-auth-gap finding, an explicit statement of whether the excluded path's own handler independently verifies the session — never approve on the assumption it does, - evidence level per finding (`repo evidence`, `documentation-based`, or `inference`), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited, - verdict (approve / approve-with-notes / block), - open questions or scope the review could not cover (e.g., "confirming actual cross-origin CSRF success requires a live cross-origin request, not static review").
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.