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.

P0 Invariants (Critical)

These issues can lead to immediate security breaches if exploited.

P0-MISSING-AUTHZ

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 } });
}

P0-MISSING-RATE-LIMIT

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);
}

P0-RACE-CONDITION

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 } },
    });
  });
}

P0-MISSING-IDEMPOTENCY

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' },
  });
}

P0-MISSING-INPUT-VALIDATION

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.

P1-MISSING-CACHE-INVALIDATION

Permission caches must be invalidated when access is revoked.

P1-MISSING-AUDIT-LOG

Security-sensitive operations should be logged for compliance and incident response.

P1-HARDCODED-SECRET

Secrets must not be committed to source code.

P1-MISSING-ERROR-HANDLING

Errors must be handled gracefully without exposing internals.

P1-INSECURE-DEFAULT

Security features should be enabled by default.

Learn More

Use the CLI to get detailed explanations:

securitychecks explain P0-MISSING-AUTHZ