diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..b1c4599 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-02-27 - Replace Insecure Math.random with CSPRNG +**Vulnerability:** Weak PRNG `Math.random()` was being used to generate coupon codes, store credit codes, and combo IDs. `Math.random()` is not cryptographically secure and can be predictable. +**Learning:** For any code, token, or ID generation that requires uniqueness and unguessability, a cryptographically secure pseudo-random number generator (CSPRNG) must be used. +**Prevention:** Always use `crypto.getRandomValues()` (via `generateSecureCode` from `lib/utils.ts`) instead of `Math.random()` for code, token, and secret generation to ensure cryptographically secure randomness. diff --git a/app/api/bargain/route.ts b/app/api/bargain/route.ts index cd182da..940c89b 100644 --- a/app/api/bargain/route.ts +++ b/app/api/bargain/route.ts @@ -4,6 +4,7 @@ import { db, user, coupons, bargainSessions, products, combos } from "@/lib/db"; import { and, eq, inArray } from "drizzle-orm"; import { headers } from "next/headers"; import { auth } from "@/lib/auth"; +import { generateSecureCode } from "@/lib/utils"; export const maxDuration = 30; const MAX_NEGOTIATION_ROUNDS = 10; @@ -20,12 +21,7 @@ type BargainCartItem = { // Generate unique coupon code function generateCouponCode(): string { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let code = "BRG-"; - for (let i = 0; i < 6; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return code; + return generateSecureCode("BRG-", 6); } function calculateCartRuleCap(cartTotal: number, isFirstTimeUser: boolean): number { diff --git a/lib/actions/admin.ts b/lib/actions/admin.ts index 54c3848..9a1c424 100644 --- a/lib/actions/admin.ts +++ b/lib/actions/admin.ts @@ -5,6 +5,7 @@ import { products, productVariants, coupons, orders, orderItems } from "@/lib/db import { requireAdmin } from "@/lib/auth-server"; import { eq, desc, sql, and, gte } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { generateSecureCode } from "@/lib/utils"; // ============================================ // PRODUCT ACTIONS @@ -567,11 +568,7 @@ export async function issueStoreCredit(data: IssueStoreCreditInput) { const creditAmount = Math.round(data.refundAmount * bonusMultiplier); // Generate unique store credit code - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let code = "CREDIT-"; - for (let i = 0; i < 8; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } + const code = generateSecureCode("CREDIT-", 8); // Set validity (30-60 days) const validityDays = Math.min(Math.max(data.validityDays || 30, 30), 60); diff --git a/lib/bargain-discount.ts b/lib/bargain-discount.ts index c8c0862..f6a6df6 100644 --- a/lib/bargain-discount.ts +++ b/lib/bargain-discount.ts @@ -9,5 +9,5 @@ export function formatBargainDiscountLabel(maxBargainDiscount: string | number | ? value.toLocaleString("en-IN") : value.toLocaleString("en-IN", { maximumFractionDigits: 2 }); - return `Bargain upto ₹${formatted}`; + return `Bargain up to ₹${formatted}`; } diff --git a/lib/cart-context.tsx b/lib/cart-context.tsx index 47e4f67..20ec937 100644 --- a/lib/cart-context.tsx +++ b/lib/cart-context.tsx @@ -1,6 +1,7 @@ "use client" import { createContext, useContext, useState, useEffect, ReactNode } from "react" +import { generateSecureCode } from "@/lib/utils" export interface CartItem { id: string @@ -80,7 +81,7 @@ export function CartProvider({ children }: { children: ReactNode }) { } const addCombo: CartContextType["addCombo"] = (combo) => { - const comboGroupId = `combo-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + const comboGroupId = generateSecureCode(`combo-${Date.now()}-`, 6).toLowerCase() setItems((prev) => [ ...prev, ...combo.items.map((item) => ({ diff --git a/lib/utils.ts b/lib/utils.ts index bd0c391..a670a88 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -4,3 +4,19 @@ import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +export function generateSecureCode(prefix: string, length: number): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const maxValid = 256 - (256 % chars.length) + let result = prefix + + while (result.length < prefix.length + length) { + const array = new Uint8Array(1) + crypto.getRandomValues(array) + if (array[0] < maxValid) { + result += chars[array[0] % chars.length] + } + } + + return result +}