security: fail-fast dev-bypass flag in production (#42)
Both auth.ts and trpc.ts now delegate the E2E_TEST_MODE-in-production check to a single shared helper (packages/api/src/lib/runtime-security.ts). trpc.ts used to only console.warn; it now throws at module load time, matching the behaviour already enforced by assertSecureRuntimeEnv on the auth side. A future refactor can no longer silently drop the guard on either side. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertNoDevBypassInProduction,
|
||||
getDevBypassViolations,
|
||||
isE2eBypassActive,
|
||||
} from "../runtime-security.js";
|
||||
|
||||
describe("runtime-security — dev-bypass fail-fast", () => {
|
||||
it("returns no violations when E2E_TEST_MODE unset", () => {
|
||||
expect(getDevBypassViolations({ NODE_ENV: "production" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns no violations in non-production env even with E2E_TEST_MODE=true", () => {
|
||||
expect(getDevBypassViolations({ NODE_ENV: "development", E2E_TEST_MODE: "true" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("flags a violation for E2E_TEST_MODE=true + NODE_ENV=production", () => {
|
||||
const violations = getDevBypassViolations({
|
||||
NODE_ENV: "production",
|
||||
E2E_TEST_MODE: "true",
|
||||
});
|
||||
expect(violations.length).toBe(1);
|
||||
expect(violations[0]).toMatch(/E2E_TEST_MODE/);
|
||||
});
|
||||
|
||||
it("assertNoDevBypassInProduction throws on prod+E2E", () => {
|
||||
expect(() =>
|
||||
assertNoDevBypassInProduction({ NODE_ENV: "production", E2E_TEST_MODE: "true" }),
|
||||
).toThrow(/E2E_TEST_MODE/);
|
||||
});
|
||||
|
||||
it("assertNoDevBypassInProduction is a no-op when E2E disabled in prod", () => {
|
||||
expect(() => assertNoDevBypassInProduction({ NODE_ENV: "production" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("isE2eBypassActive only true in non-production", () => {
|
||||
expect(isE2eBypassActive({ NODE_ENV: "development", E2E_TEST_MODE: "true" })).toBe(true);
|
||||
expect(isE2eBypassActive({ NODE_ENV: "production", E2E_TEST_MODE: "true" })).toBe(false);
|
||||
expect(isE2eBypassActive({ NODE_ENV: "development" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Shared fail-fast checks for dev-only bypass flags.
|
||||
*
|
||||
* Both `apps/web/src/server/runtime-env.ts` and `packages/api/src/trpc.ts`
|
||||
* gate behaviour on `E2E_TEST_MODE`. Historically each had its own check
|
||||
* (one throwing, one `console.warn`-ing), which meant a refactor that
|
||||
* dropped one import silently re-enabled the bypass in production. This
|
||||
* module is the single source of truth; both call sites delegate here.
|
||||
*
|
||||
* CapaKraken security ticket #42 / EAPPS 3.2.7.04.
|
||||
*/
|
||||
|
||||
type RuntimeEnv = Partial<Record<string, string | undefined>>;
|
||||
|
||||
const DEV_BYPASS_FLAGS = ["E2E_TEST_MODE"] as const;
|
||||
|
||||
export function isE2eBypassActive(env: RuntimeEnv = process.env): boolean {
|
||||
return env["E2E_TEST_MODE"] === "true" && env["NODE_ENV"] !== "production";
|
||||
}
|
||||
|
||||
export function getDevBypassViolations(env: RuntimeEnv = process.env): string[] {
|
||||
if (env["NODE_ENV"] !== "production") return [];
|
||||
const out: string[] = [];
|
||||
for (const flag of DEV_BYPASS_FLAGS) {
|
||||
if (env[flag] === "true") {
|
||||
out.push(
|
||||
`${flag} must not be 'true' in production — it disables rate limiting and session controls.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function assertNoDevBypassInProduction(env: RuntimeEnv = process.env): void {
|
||||
const violations = getDevBypassViolations(env);
|
||||
if (violations.length === 0) return;
|
||||
throw new Error(`[FATAL] Dev-bypass flag set in production: ${violations.join(" ")}`);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { prisma, Prisma } from "@capakraken/db";
|
||||
import { resolvePermissions, PermissionKey, SystemRole } from "@capakraken/shared";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import { ZodError } from "zod";
|
||||
import { assertNoDevBypassInProduction, isE2eBypassActive } from "./lib/runtime-security.js";
|
||||
import { loggingMiddleware } from "./middleware/logging.js";
|
||||
import { apiRateLimiter } from "./middleware/rate-limit.js";
|
||||
|
||||
@@ -139,12 +140,11 @@ const withPrismaErrors = t.middleware(async ({ next }) => {
|
||||
throw error; // re-throw non-Prisma errors unchanged
|
||||
}
|
||||
});
|
||||
const isE2eTestMode =
|
||||
process.env["E2E_TEST_MODE"] === "true" && process.env["NODE_ENV"] !== "production";
|
||||
if (process.env["E2E_TEST_MODE"] === "true" && process.env["NODE_ENV"] === "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[SECURITY] E2E_TEST_MODE is set in production — rate limiting is NOT bypassed.");
|
||||
}
|
||||
// Fail-fast if a dev-bypass flag is left on in a production build. A warning
|
||||
// is not enough — historically a refactor that drops an import can silently
|
||||
// re-enable the bypass. See packages/api/src/lib/runtime-security.ts.
|
||||
assertNoDevBypassInProduction();
|
||||
const isE2eTestMode = isE2eBypassActive();
|
||||
|
||||
/**
|
||||
* Protected procedure — requires authenticated session AND a valid DB user record.
|
||||
|
||||
Reference in New Issue
Block a user