Back to skill

Security audit

nextjs-app-router

Security checks for vulnerabilities and agentic risk

Overview

This Next.js scaffolding skill is coherent, but review is warranted because some generated authentication and data-mutation templates contain security weaknesses.

Install only if you are comfortable reviewing the generated code before use. In particular, validate returnTo redirects to same-origin local paths, enforce ownership in the database mutation predicate for update/delete handlers, pin reviewed dependency and CLI versions where possible, and approve database or Docker commands only against a development database.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
references/templates/auth-slice.md:27
Finding
Unvalidated Post-Authentication Navigation Target<![CDATA[ ## Vulnerability Details **File Location**: `references/templates/auth-slice.md:27-33` **Additional Locations**: `references/templates/form-with-zod.md:152-168`, `references/templates/root-layout.md:149-165` **Vulnerability Type**: Unvalidated redirect/navigation target **Risk Level**: High ### Vulnerable Code ```tsx const res = await signIn('credentials', { email, password, redirect: false }); if (res?.error) { // surface "Invalid email or password." — don't leak which field was wrong return; } router.replace(searchParams.get('returnTo') ?? '/app/dashboard'); router.refresh(); ``` The equivalent full data flow in `references/templates/form-with-zod.md` is: ```tsx const returnTo = searchParams.get('returnTo') ?? '/app/dashboard'; async function onSubmit(values: LoginValues) { const res = await signIn('credentials', { ...values, redirect: false }); if (!res || res.error) { setError('Invalid email or password.'); return; } router.replace(returnTo); router.refresh(); } ``` ### Technical Analysis The generated login form reads `returnTo` directly from the URL query string and passes it to `router.replace()` without confirming that it is a safe, local application path. Although the middleware-generated value is normally a pathname, clients are not required to reach the login page through middleware. An attacker can construct a login URL containing an arbitrary `returnTo` value. Next.js explicitly treats untrusted values passed to router navigation methods as unsafe; behavior for absolute URLs and dangerous URI schemes can also vary by framework/browser version. The application must not rely on middleware as validation because the query parameter remains attacker-controlled at the navigation sink. ### Attack Path 1. An attacker creates a URL such as `/auth/login?returnTo=<attacker-controlled-target>`. 2. The attacker sends the URL to a victim. 3. The victim enters valid credentials and authentication succeeds. 4. The gene ...[truncated 996 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Centralize validation in a helper and permit only local absolute paths: ```ts export function safeReturnTo( value: string | null, fallback = '/app/dashboard', ): string { if (!value) return fallback; if (!value.startsWith('/') || value.startsWith('//')) return fallback; if (value.includes('\\') || /[\u0000-\u001F\u007F]/.test(value)) return fallback; try { const parsed = new URL(value, window.location.origin); if (parsed.origin !== window.location.origin) return fallback; return `${parsed.pathname}${parsed.search}${parsed.hash}`; } catch { return fallback; } } ``` Use the validated result at every navigation sink: ```tsx const returnTo = safeReturnTo(searchParams.get('returnTo')); router.replace(returnTo); ``` Additional hardening: - Apply the same helper in `auth-slice.md`, `form-with-zod.md`, and `root-layout.md`. - Restrict destinations further to known authenticated route prefixes such as `/app/` and `/admin/` where appropriate. - Add tests covering absolute external URLs, protocol-relative URLs, backslashes, control characters, encoded variants, and dangerous URI schemes. - Keep the fallback fixed and application-owned. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
references/templates/route-handler.md:68
Finding
Non-Atomic Ownership Enforcement in Update and Delete Handlers<![CDATA[ ## Vulnerability Details **File Location**: `references/templates/route-handler.md:68-105` **Vulnerability Type**: Time-of-check/time-of-use authorization race **Risk Level**: Medium ### Vulnerable Code ```ts export const PATCH = withApiErrors(async (req: Request, ctx: RouteContext) => { const session = await requireSession(); const { id } = await ctx.params; const body = await req.json().catch(() => null); const parsed = {{feature}}Schema.partial().safeParse(body); if (!parsed.success) { return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); } // Ownership check via findFirst + userId predicate. const existing = await db.{{feature}}.findFirst({ where: { id, userId: session.user.id }, select: { id: true }, }); if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }); const updated = await db.{{feature}}.update({ where: { id }, data: parsed.data, }); return NextResponse.json(updated); }); export const DELETE = withApiErrors(async (_req: Request, ctx: RouteContext) => { const session = await requireSession(); const { id } = await ctx.params; const existing = await db.{{feature}}.findFirst({ where: { id, userId: session.user.id }, select: { id: true }, }); if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 }); await db.{{feature}}.delete({ where: { id } }); return new NextResponse(null, { status: 204 }); }); ``` ### Technical Analysis The handler verifies ownership using `findFirst({ where: { id, userId } })`, but performs the subsequent mutation using only `where: { id }`. Authorization and mutation are therefore separate database operations. If ownership or another security-relevant property changes between these operations, the authorization decision no longer describes the record being updated or deleted. The database mutation itself does not enforce the authenticated user's identity. This is a cla ...[truncated 1404 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Enforce ownership in the mutation predicate itself. For example: ```ts const result = await db.{{feature}}.updateMany({ where: { id, userId: session.user.id, }, data: parsed.data, }); if (result.count !== 1) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } const updated = await db.{{feature}}.findFirst({ where: { id, userId: session.user.id, }, }); return NextResponse.json(updated); ``` For deletion: ```ts const result = await db.{{feature}}.deleteMany({ where: { id, userId: session.user.id, }, }); if (result.count !== 1) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } return new NextResponse(null, { status: 204 }); ``` Where returning the updated row atomically is required, use a database transaction with appropriate isolation and revalidate ownership inside that transaction. Additional measures: - Include all authorization-relevant fields in the mutation predicate. - Do not treat a preceding read as sufficient authorization for a later write. - Add concurrency tests that change ownership between authorization and mutation. - Preserve the `404` response to avoid disclosing the existence of another user's record. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:72
Finding
Execution of Mutable and Unreviewed Third-Party Package Releases<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:72-80` **Additional Locations**: `SKILL.md:106-116`, `references/templates/ci-and-hooks.md:98-102`, `references/templates/ci-and-hooks.md:118-121` **Vulnerability Type**: Software supply-chain exposure **Risk Level**: Medium ### Vulnerable Code ```text 6. Concrete resolution command when context7 is unavailable: `npm view <pkg> version` (e.g. `npm view next version`; `npm view next-auth dist-tags` — plain `npm view next-auth version` returns the latest stable, which may still be v4; never scaffold v4). Never write a version you did not just resolve this session — no versions from memory, no `latest`/`^latest`, no invented numbers. - The shadcn CLI is `npx shadcn@latest` (`init` / `add`); the old `shadcn-ui` package name is dead. ``` The generated CI instructions also execute installed third-party tooling: ```yaml - run: pnpm install --frozen-lockfile - run: pnpm exec prisma migrate deploy - run: pnpm exec prisma generate - run: pnpm db:seed - run: pnpm exec playwright install --with-deps chromium ``` Local hook initialization includes: ```bash pnpm exec husky init ``` ### Technical Analysis The Skill explicitly invokes `npx shadcn@latest`, which resolves and executes a mutable package tag. The code executed by that command can change after the Skill itself has been reviewed. Dependency installation also permits package lifecycle scripts, while Prisma, Playwright, Husky, and other package-provided executables run with the permissions of the developer or CI runner. Version resolution immediately before scaffolding improves compatibility but does not establish trust or provide a review boundary. `playwright install --with-deps` expands the impact because it installs browser and system dependencies in CI. Whether elevated operating-system privileges are used depends on the runner configuration. No evidence was found that the named packages are currently malicious. The vulnerability is th ...[truncated 1199 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace `npx shadcn@latest` with an exact, reviewed version. - Pin the package-manager version and all direct dependencies to exact versions rather than mutable tags. - Generate and commit a lockfile, then require frozen/immutable lockfile installation locally and in CI. - Review package provenance, maintainers, signatures or attestations, and lockfile changes before execution. - Use an approved registry or registry proxy with package allowlisting and malware scanning. - Run scaffolding and package installation in a sandbox with limited filesystem and network access. - Minimize CI token permissions and do not expose secrets to dependency-install jobs unless required. - Separate dependency installation from privileged system-package installation. - Use a prebuilt Playwright container or reviewed runner image instead of invoking `--with-deps` in a broadly privileged job. - Consider disabling lifecycle scripts during the initial install and explicitly running only reviewed setup commands afterward where package compatibility permits. ]]>
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The skill goes beyond generating code and instructs the agent to run package installs, Docker, Prisma migrations, and secret-generation commands on the user's machine. Even with some confirmation gates, this materially increases operational risk because a broad-triggered skill may perform environment-changing actions that the user did not clearly intend when they only asked for advice or conventions.

Description-Behavior Mismatch

Low
Confidence
88% confidence
Finding
The manifest says to invoke the skill for vague phrases like 'my Next.js conventions' even if the user does not explicitly request scaffolding. Because the skill can later write files and run commands, this broad auto-activation can cause actions outside the user's intent boundary.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The form reads a client-controlled `returnTo` query parameter and passes it directly to `router.replace(returnTo)` after successful authentication, with no validation that the destination is same-origin or within an approved path set. In a scaffold template, this can propagate an open-redirect pattern into generated apps, enabling phishing flows or post-login redirection to attacker-chosen locations if the framework/router accepts such destinations.

Vague Triggers

Medium
Confidence
91% confidence
Finding
Overly broad trigger phrases increase the chance that this high-impact skill is selected for ordinary Next.js questions. In context, that is dangerous because the skill is not read-only: it can generate many files, install dependencies, and initiate local infrastructure and database steps.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The 'When to use this skill' section includes ambiguous activators like wiring screens or using preferred patterns, which can overlap with many normal development tasks. In a skill that may modify the repository and execute commands, ambiguous activation widens the blast radius of accidental invocation.

Static analysis

No suspicious patterns detected.