Ban any at Trust Boundaries — Use unknown with Validation
Share
Using 'any' or 'as any' at API boundaries, form handlers, and external data silently disables TypeScript safety, causing runtime crashes from unexpected data. [CWE-20]
Why This Matters
prevents runtime crashes from untyped external data that bypasses compile-time checks
Ban any at Trust Boundaries — Use unknown with Validation
Impact: HIGH (prevents runtime crashes from untyped external data that bypasses compile-time checks)
any silently turns off TypeScript at exactly the places where type safety matters most — system boundaries where external data enters your application. When you cast API responses, form data, URL params, or webhook payloads as any, TypeScript stops checking the entire downstream call chain. The app compiles cleanly but crashes at runtime when the actual data doesn't match your assumptions.
This complements qual-validate-boundaries: even if you validate, casting to any anywhere in the chain nullifies the validation.
Incorrect (any at boundaries):
// ❌ any spreads like a virus — disables type checking for everything it touchesexport async function GET() { const res = await fetch('https://api.example.com/users') const data: any = await res.json() // TypeScript gives up here // No errors shown, but all of these could crash at runtime: const name = data.users[0].profile.displayName // Cannot read property of undefined const email = data.users[0].email.toLowerCase() // .toLowerCase of undefined return NextResponse.json({ name, email })}
// ❌ 'as any' to suppress TypeScript errors — hides real bugsexport async function POST(request: NextRequest) { const body = await request.json() const user = body as any await db.user.create({ data: user }) // Passes ANYTHING to the database}
// ❌ any in Server Actions — parameters come from untrusted network requests'use server'export async function updateSettings(settings: any) { await db.settings.update({ data: settings }) // Prototype pollution risk}
// ❌ Catch-all any in utility functions at boundariesfunction processWebhook(payload: any) { // Every downstream function now accepts any shape updateOrder(payload.order) notifyUser(payload.user) updateInventory(payload.items)}
Correct (unknown + validation):
// ✅ unknown forces you to validate before useimport { z } from 'zod'const UsersResponseSchema = z.object({ users: z.array(z.object({ profile: z.object({ displayName: z.string() }), email: z.string().email(), })),})export async function GET() { const res = await fetch('https://api.example.com/users') const json: unknown = await res.json() const data = UsersResponseSchema.parse(json) // Throws descriptive error if wrong const { displayName } = data.users[0].profile // TypeScript knows the shape const { email } = data.users[0] // Autocomplete works correctly return NextResponse.json({ name: displayName, email })}
// ✅ Server Actions: unknown parameter + schema validation'use server'import { z } from 'zod'const SettingsSchema = z.object({ theme: z.enum(['light', 'dark']), language: z.string().min(2).max(5), notifications: z.boolean(),})export async function updateSettings(rawInput: unknown) { const settings = SettingsSchema.parse(rawInput) await db.settings.update({ data: settings }) // Only validated fields reach DB}
// ✅ Webhook handler with proper typingconst WebhookPayloadSchema = z.discriminatedUnion('event', [ z.object({ event: z.literal('order.completed'), order: z.object({ id: z.string(), total: z.number() }), }), z.object({ event: z.literal('user.created'), user: z.object({ id: z.string(), email: z.string() }), }),])export async function POST(request: NextRequest) { const json: unknown = await request.json() const payload = WebhookPayloadSchema.parse(json) // TypeScript narrows the type based on the event discriminator if (payload.event === 'order.completed') { await updateOrder(payload.order) // TypeScript knows .order exists }}
The any → unknown migration path:
Change the type from any to unknown
TypeScript will show errors everywhere the value is used without narrowing