Injection guards

MongoDB operator injection prevented by stripping $-prefixed keys. Supabase uses parameterized queries. Zod validates types before any DB call.

NoSQL injection

MongoDB queries accept JavaScript objects - which means an attacker who can control the shape of a query object can inject MongoDB operators. A classic example:

// Attacker sends: { "email": { "$gt": "" }, "password": { "$gt": "" } }
// Without protection, this matches any user:
await UserModel.findOne({
	email: { $gt: '' },        // matches all documents
	password: { $gt: '' },     // matches all documents
})

This would bypass authentication entirely - findOne() returns the first user in the collection regardless of credentials.

stripMongoOperators()

stripMongoOperators() in lib/sanitize.ts recursively removes any object key that starts with $ or contains . before the data reaches any DB query:

lib/sanitize.ts
export function stripMongoOperators(obj: unknown): unknown {
	if (typeof obj !== 'object' || obj === null) return obj
	if (Array.isArray(obj)) return obj.map(stripMongoOperators)

	return Object.fromEntries(
		Object.entries(obj as Record<string, unknown>)
			.filter(([key]) => !key.startsWith('$') && !key.includes('.'))
			.map(([key, value]) => [key, stripMongoOperators(value)])
	)
}

After running through stripMongoOperators(), the injection attempt above becomes:

// $gt key is stripped - the object is now empty strings
{ email: '', password: '' }
// No user matches - injection neutralized

Explicit sanitization

The current MongoDB variant does not depend on a global mongoose-sanitize plugin. Instead, sanitization is done explicitly in application logic before queries run, which avoids plugin-level runtime surprises and keeps the active defense visible in code.

SQL injection (Supabase)

The Supabase JavaScript client generates parameterized SQL queries internally. User values are always passed as bound parameters - never interpolated into the query string:

// This generates: SELECT * FROM users WHERE email = $1
// With $1 bound to the sanitized email string
const { data } = await supabase
	.from('users')
	.select('*')
	.eq('email', sanitizedEmail)

There is no raw SQL string construction in the Supabase variant of NextForge. All queries use the fluent client API.

Zod as a defense layer

Zod validation runs after sanitization. Because Zod enforces strict types (string, email format, minimum length), even if a $-prefixed key somehow survived sanitization, Zod would reject the input before it reached a DB call:

lib/validate.ts
export const loginSchema = z.object({
	// email must be a valid email string - objects or operators rejected
	email: z.string().email().toLowerCase().trim(),
	// password must be a string of at least 8 chars
	password: z.string().min(8),
})