Invariants Overview
Understanding the security invariants that SecurityChecks validates.
Security Invariants
SecurityChecks validates invariants - security properties that should always hold true in your codebase. When an invariant is violated, it indicates a potential security vulnerability.
We currently check 109 invariants across authorization, data flow, webhooks, cryptography, and framework-specific patterns.
P0 Invariants (Critical)
These issues can lead to immediate security breaches if exploited.
AUTHZ.SERVICE_LAYER.ENFORCED
Missing Authorization Check
Every endpoint that accesses or modifies user data must verify the requester has permission.
// Vulnerable
export async function GET(req: Request) {
const userId = req.params.id;
return await db.user.findUnique({ where: { id: userId } });
}
// Fixed
export async function GET(req: Request) {
const { userId: currentUserId } = await auth();
const targetUserId = req.params.id;
// Verify permission
if (currentUserId !== targetUserId && !isAdmin(currentUserId)) {
return new Response('Forbidden', { status: 403 });
}
return await db.user.findUnique({ where: { id: targetUserId } });
}
RATE_LIMIT.MISSING
Missing Rate Limiting
Authentication and sensitive endpoints must be rate limited to prevent brute force attacks.
// Vulnerable
export async function POST(req: Request) {
const { email, password } = await req.json();
return await attemptLogin(email, password);
}
// Fixed
import { Ratelimit } from '@upstash/ratelimit';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'),
});
export async function POST(req: Request) {
const ip = req.headers.get('x-forwarded-for');
const { success } = await ratelimit.limit(ip);
if (!success) {
return new Response('Too many requests', { status: 429 });
}
const { email, password } = await req.json();
return await attemptLogin(email, password);
}
RACE.BALANCE_CHECK
Race Condition in Transaction
Operations that read-modify-write must be atomic to prevent exploitation.
// Vulnerable
async function transfer(from: string, to: string, amount: number) {
const fromBalance = await getBalance(from);
if (fromBalance < amount) throw new Error('Insufficient funds');
await updateBalance(from, fromBalance - amount);
await updateBalance(to, await getBalance(to) + amount);
}
// Fixed
async function transfer(from: string, to: string, amount: number) {
await prisma.$transaction(async (tx) => {
const fromAccount = await tx.account.findUnique({
where: { id: from },
});
if (fromAccount.balance < amount) {
throw new Error('Insufficient funds');
}
await tx.account.update({
where: { id: from },
data: { balance: { decrement: amount } },
});
await tx.account.update({
where: { id: to },
data: { balance: { increment: amount } },
});
});
}
WEBHOOK.IDEMPOTENT
Missing Idempotency in Webhook Handler
Webhook handlers must check for duplicate deliveries to prevent double-processing.
// Vulnerable
export async function POST(req: Request) {
const event = await req.json();
await processPayment(event.data);
}
// Fixed
export async function POST(req: Request) {
const event = await req.json();
const eventId = event.id;
// Check if already processed
const existing = await db.processedEvent.findUnique({
where: { id: eventId },
});
if (existing) {
return new Response('Already processed', { status: 200 });
}
// Mark as processing
await db.processedEvent.create({
data: { id: eventId, status: 'processing' },
});
await processPayment(event.data);
await db.processedEvent.update({
where: { id: eventId },
data: { status: 'completed' },
});
}
DATAFLOW.UNTRUSTED.SQL_QUERY
Missing Input Validation
All user input must be validated before use.
// Vulnerable
export async function POST(req: Request) {
const { amount, currency } = await req.json();
return await createPayment(amount, currency);
}
// Fixed
import { z } from 'zod';
const paymentSchema = z.object({
amount: z.number().positive().max(1000000),
currency: z.enum(['USD', 'EUR', 'GBP']),
});
export async function POST(req: Request) {
const body = await req.json();
const result = paymentSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: 'Invalid input', details: result.error.flatten() },
{ status: 400 }
);
}
return await createPayment(result.data.amount, result.data.currency);
}
P1 Invariants (Important)
These issues should be fixed but may not lead to immediate exploitation.
CACHE.INVALIDATION.ON_AUTH_CHANGE
Permission caches must be invalidated when access is revoked.
LOGGING.SENSITIVE.DATA
Security-sensitive operations should be logged for compliance and incident response.
CONFIG.ENV_HARDCODED
Secrets must not be committed to source code.
ERROR.SECRET_LEAK
Errors must be handled gracefully without exposing internals.
SESSION.COOKIE.NO_SECURE
Security features should be enabled by default.
Learn More
Use the CLI to get detailed explanations:
scheck explain AUTHZ.SERVICE_LAYER.ENFORCED