ubon
Version:
Security scanner for AI-generated apps (Cursor, Lovable, Windsurf, v0). Catches hardcoded secrets, prompt injection, hallucinated imports, Server Actions / Edge runtime mistakes, and the vibe-coded vulnerabilities traditional linters miss.
239 lines • 14.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.frameworkRules = void 0;
const make = (meta, fileTypes) => ({
meta,
impl: {
fileTypes: fileTypes || ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'svelte', 'astro']
}
});
/**
* Modern framework rule pack — Next 14/15 Server Actions, Edge runtime,
* SvelteKit, Astro, Remix, Hono, Drizzle and Prisma. Detection lives in
* `FrameworkScanner` (regex/heuristic, not full AST). Metadata is centralised
* here so `ubon explain <id>` works for every rule.
*/
exports.frameworkRules = {
// ---- Next.js Server Actions (App Router 14/15) -----------------------
NEXT212: make({
id: 'NEXT212',
category: 'security',
severity: 'high',
message: 'Server Action exported without an auth check',
fix: 'Call `auth()` / `getServerSession()` (or your equivalent) at the top of every exported Server Action and throw on missing session.',
impact: "A Server Action is a public POST endpoint. Without an auth check it can be invoked by any visitor — even ones who never rendered the page that uses it.",
helpUri: 'https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#authentication-and-authorization'
}),
NEXT213: make({
id: 'NEXT213',
category: 'security',
severity: 'high',
message: 'Server Action accepts FormData / JSON without input validation',
fix: 'Parse args through zod/valibot/yup before touching the database.',
impact: 'Server Actions accept arbitrary client payloads; missing validation lets attackers smuggle unexpected fields into your ORM (mass assignment).',
helpUri: 'https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#validation'
}),
NEXT214: make({
id: 'NEXT214',
category: 'security',
severity: 'high',
message: '`use server` module imported from a `use client` file',
fix: 'Call Server Actions through the Action object or via a server component prop. Never import the action module directly into client code.',
impact: 'Direct imports leak server-only code (DB clients, secrets) into the client bundle.',
helpUri: 'https://react.dev/reference/rsc/use-server'
}),
NEXT215: make({
id: 'NEXT215',
category: 'security',
severity: 'high',
message: 'Server Action passes user input directly to redirect()/revalidatePath()',
fix: 'Validate the path against an allowlist before redirect()/revalidatePath().',
impact: 'Open redirects and arbitrary cache invalidations can be triggered by any unauthenticated visitor.',
helpUri: 'https://nextjs.org/docs/app/api-reference/functions/redirect'
}),
NEXT216: make({
id: 'NEXT216',
category: 'security',
severity: 'high',
message: 'App Router page/layout types `params`/`searchParams` as a plain object (Next 15 requires a Promise)',
fix: 'Type the prop as `Promise<{ ... }>` and `await` it before use. Example: `export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; }`.',
impact: 'In Next 15 `params` and `searchParams` are asynchronous. A synchronous object type compiles but returns a thenable at runtime, so field access silently yields `undefined` (or crashes under strict typing).',
helpUri: 'https://nextjs.org/docs/app/api-reference/file-conventions/page#params-optional'
}, ['ts', 'tsx']),
NEXT217: make({
id: 'NEXT217',
category: 'security',
severity: 'high',
message: "React hook used in a component file without the `'use client'` directive",
fix: "Add `'use client';` at the very top of the file (above the imports).",
impact: "Using `useState`/`useEffect`/etc. in a Server Component fails the Next build. Without `'use client'`, the file is treated as server-side and the hook import throws at compile time.",
helpUri: 'https://nextjs.org/docs/app/building-your-application/rendering/client-components'
}, ['tsx', 'jsx']),
NEXT218: make({
id: 'NEXT218',
category: 'security',
severity: 'low',
message: '`reactStrictMode: false` disables an important dev-time correctness check',
fix: 'Remove the override or set `reactStrictMode: true`. Strict mode surfaces unsafe side-effects and legacy lifecycles while developing.',
impact: 'Disabling Strict Mode masks double-invocation bugs and deprecated API warnings that Next would otherwise catch during `next dev`.',
helpUri: 'https://react.dev/reference/react/StrictMode'
}, ['js', 'ts', 'mjs', 'cjs']),
NEXT219: make({
id: 'NEXT219',
category: 'security',
severity: 'low',
message: '`experimental.serverActions: true` is the Next 13 shape and is ignored in Next 14/15',
fix: 'Remove the flag. Server Actions are stable since Next 14; the boolean form no longer applies. Use `experimental.serverActions = { allowedOrigins, bodySizeLimit }` only if you need the object form.',
impact: 'Stale experimental flags give a false sense that something is configured. Keeping the boolean shape masks the fact that no tuning is actually in place.',
helpUri: 'https://nextjs.org/docs/app/api-reference/next-config-js/serverActions'
}, ['js', 'ts', 'mjs', 'cjs']),
NEXT220: make({
id: 'NEXT220',
category: 'security',
severity: 'high',
message: '`typeof window !== "undefined"` / `window.*` access inside a Server Component',
fix: "Move browser access into a `'use client'` component, or guard with `if (typeof document !== 'undefined')` only for progressive enhancement.",
impact: "Server Components run at build/request time in Node. A `window` reference there crashes the render; branching silently removes the branch that actually does the work.",
helpUri: 'https://nextjs.org/docs/app/building-your-application/rendering/server-components'
}, ['tsx', 'jsx']),
NEXT221: make({
id: 'NEXT221',
category: 'security',
severity: 'high',
message: "Client Component imports a server-only module (`fs`, `child_process`, `pg`, `better-sqlite3`, `@prisma/client`, …)",
fix: "Move the DB / filesystem access to a Server Component, Route Handler, or Server Action and pass data down as a prop.",
impact: "Server-only modules have no browser equivalent. Bundling one into a client chunk either crashes at runtime or (worse) leaks your database credentials into the bundle.",
helpUri: 'https://nextjs.org/docs/app/building-your-application/rendering/client-components'
}, ['tsx', 'jsx']),
NEXT222: make({
id: 'NEXT222',
category: 'security',
severity: 'medium',
message: 'Server Component imports a client-only state library (`zustand` / `jotai` / `recoil` / `valtio`)',
fix: "Move stateful logic into a `'use client'` component and pass initial data via props.",
impact: "Client state libraries rely on React hooks that only run on the client. Importing them from a Server Component explodes at build time or silently no-ops at runtime.",
helpUri: 'https://react.dev/reference/rsc/use-client'
}, ['tsx', 'jsx']),
NEXT223: make({
id: 'NEXT223',
category: 'security',
severity: 'high',
message: "`app/**/route.ts` uses `export default` instead of a named `GET`/`POST`/… export",
fix: 'Rename the default export to `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, or `OPTIONS` as appropriate.',
impact: "Next ignores default exports from route files. The route silently 404s; the AI thinks the endpoint exists because the file compiles.",
helpUri: 'https://nextjs.org/docs/app/building-your-application/routing/route-handlers'
}, ['ts', 'tsx']),
NEXT224: make({
id: 'NEXT224',
category: 'development',
severity: 'low',
message: '`<a href="/internal">` used instead of `<Link>` for an internal route',
fix: 'Import `Link` from `next/link` and use it for same-origin navigation.',
impact: "Plain `<a>` does a full page reload — loses client state, re-runs every layout, and makes the app feel like a traditional MPA.",
helpUri: 'https://nextjs.org/docs/app/api-reference/components/link'
}, ['tsx', 'jsx']),
NEXT225: make({
id: 'NEXT225',
category: 'security',
severity: 'medium',
message: '`<form method="POST" action="/api/…">` without a CSRF token or a Server Action',
fix: 'Use a Server Action (`action={createPost}`) or add a CSRF token middleware (next-auth, csrf-csrf, edge middleware).',
impact: 'Cross-origin POSTs from a logged-in victim can trigger state changes on your API — classic CSRF.',
helpUri: 'https://owasp.org/www-community/attacks/csrf'
}, ['tsx', 'jsx']),
// ---- Edge runtime ----------------------------------------------------
EDGE001: make({
id: 'EDGE001',
category: 'security',
severity: 'high',
message: 'Node-only API used in `runtime = "edge"` route',
fix: 'Remove the `fs` / `child_process` / `crypto.createHash` import or switch the route to `runtime = "nodejs"`.',
impact: 'Deploys silently break at runtime in production; Vercel/Cloudflare swallow the import error and serve a generic 500.',
helpUri: 'https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes'
}),
EDGE002: make({
id: 'EDGE002',
category: 'security',
severity: 'medium',
message: 'Long-lived response in edge runtime without streaming',
fix: 'Return a `ReadableStream` (or use `streamText`) so the runtime can keep the connection open without hitting the 25s/30s wall-clock cap.',
impact: 'Edge handlers are killed after ~25–30s; non-streaming LLM calls regularly hit this limit.'
}),
EDGE003: make({
id: 'EDGE003',
category: 'security',
severity: 'medium',
message: '`process.env.X` read at module top-level inside edge route',
fix: 'Read the env var inside the request handler. Top-level reads are baked at build time and may snapshot stale values across deploys.',
impact: 'Edge bundles freeze top-level `process.env` reads; rotating a secret will not take effect until the next build.'
}),
// ---- SvelteKit -------------------------------------------------------
SVELTE001: make({
id: 'SVELTE001',
category: 'security',
severity: 'high',
message: 'SvelteKit `+page.server.ts` returns env/secret in `load` data',
fix: 'Strip env vars and secrets from the load() return value; only return DTOs the page needs.',
impact: "Anything returned from a server `load()` is serialised into the page's HTML and visible in the browser.",
helpUri: 'https://kit.svelte.dev/docs/load'
}, ['ts', 'js']),
SVELTE002: make({
id: 'SVELTE002',
category: 'security',
severity: 'high',
message: 'SvelteKit form action without CSRF/auth verification',
fix: 'Use the built-in `event.locals.user` check or `event.request.formData()` with origin verification.',
impact: 'Form actions are POST endpoints; missing auth = mass assignment + IDOR.',
helpUri: 'https://kit.svelte.dev/docs/form-actions'
}, ['ts', 'js']),
// ---- Astro -----------------------------------------------------------
ASTRO001: make({
id: 'ASTRO001',
category: 'security',
severity: 'high',
message: 'Astro server endpoint without auth check',
fix: 'Validate `Astro.locals.user` (or your auth helper) before mutating data in `POST`/`PUT`/`DELETE`.',
impact: 'Astro `endpoint.ts` files are unauthenticated by default.',
helpUri: 'https://docs.astro.build/en/core-concepts/endpoints/'
}, ['ts', 'js', 'astro']),
// ---- Remix -----------------------------------------------------------
REMIX001: make({
id: 'REMIX001',
category: 'security',
severity: 'high',
message: 'Remix `action`/`loader` accesses DB without session check',
fix: 'Call `requireUserId(request)` or your equivalent at the top of every action/loader that mutates data.',
impact: 'Loaders and actions are public endpoints; missing auth leaks data and allows mutations.',
helpUri: 'https://remix.run/docs/en/main/guides/authentication'
}, ['ts', 'js', 'tsx', 'jsx']),
// ---- Hono ------------------------------------------------------------
HONO001: make({
id: 'HONO001',
category: 'security',
severity: 'medium',
message: 'Hono route reads body without validator middleware',
fix: "Wrap the route with `zValidator('json', schema)` (or your validator) before consuming `c.req.json()`.",
impact: 'Unvalidated bodies feed unsafe values into your handlers.',
helpUri: 'https://hono.dev/guides/validation'
}, ['ts', 'js', 'tsx', 'jsx', 'mjs', 'cjs']),
// ---- Drizzle / Prisma -----------------------------------------------
DRIZZLE001: make({
id: 'DRIZZLE001',
category: 'security',
severity: 'high',
message: "Drizzle `sql\`\`` template includes interpolated user input",
fix: 'Use Drizzle helpers (`eq`, `and`, `inArray`) or `sql.placeholder` instead of raw interpolation.',
impact: 'Raw SQL interpolation is the classic SQL injection vector.',
helpUri: 'https://orm.drizzle.team/docs/sql'
}, ['ts', 'js', 'tsx', 'jsx']),
PRISMA001: make({
id: 'PRISMA001',
category: 'security',
severity: 'high',
message: 'Prisma `$queryRawUnsafe` / `$executeRawUnsafe` with untrusted input',
fix: 'Use `$queryRaw` (tagged template) with parameterised inputs, or wrap with Prisma\'s native query builders.',
impact: 'The `Unsafe` variants concatenate strings — direct SQL injection sink.',
helpUri: 'https://www.prisma.io/docs/orm/prisma-client/queries/raw-database-access'
}, ['ts', 'js', 'tsx', 'jsx'])
};
//# sourceMappingURL=index.js.map