feat: add CUID v1 validation for environment ID endpoints (#6827)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Matti Nannt
2025-11-17 08:33:52 +01:00
committed by GitHub
parent 341e2639e1
commit 71a85c7126
6 changed files with 64 additions and 20 deletions

View File

@@ -1,5 +1,6 @@
import { NextRequest } from "next/server";
import { logger } from "@formbricks/logger";
import { ZEnvironmentId } from "@formbricks/types/environment";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/environment/lib/environmentState";
import { responses } from "@/app/lib/api/response";
@@ -28,15 +29,38 @@ export const GET = withV1ApiWrapper({
const params = await props.params;
try {
// Simple validation for environmentId (faster than Zod for high-frequency endpoint)
// Basic type check for environmentId
if (typeof params.environmentId !== "string") {
return {
response: responses.badRequestResponse("Environment ID is required", undefined, true),
};
}
const environmentId = params.environmentId.trim();
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
// This catches all invalid formats including:
// - null/undefined passed as string "null" or "undefined"
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
// - Empty or whitespace-only IDs
// - Any other invalid CUID v1 format
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
if (!cuidValidation.success) {
logger.warn(
{
environmentId: params.environmentId,
url: req.url,
validationError: cuidValidation.error.errors[0]?.message,
},
"Invalid CUID v1 format detected"
);
return {
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
};
}
// Use optimized environment state fetcher with new caching approach
const environmentState = await getEnvironmentState(params.environmentId);
const environmentState = await getEnvironmentState(environmentId);
const { data } = environmentState;
return {

View File

@@ -2,7 +2,7 @@ import { headers } from "next/headers";
import { NextRequest } from "next/server";
import { UAParser } from "ua-parser-js";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { ZEnvironmentId } from "@formbricks/types/environment";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
@@ -51,7 +51,7 @@ export const POST = withV1ApiWrapper({
}
const { environmentId } = params;
const environmentIdValidation = ZId.safeParse(environmentId);
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
if (!environmentIdValidation.success) {

View File

@@ -1,7 +1,7 @@
import { headers } from "next/headers";
import { UAParser } from "ua-parser-js";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { ZEnvironmentId } from "@formbricks/types/environment";
import { InvalidInputError } from "@formbricks/types/errors";
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
@@ -43,7 +43,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
}
const { environmentId } = params;
const environmentIdValidation = ZId.safeParse(environmentId);
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
const responseInputValidation = ZResponseInputV2.safeParse({ ...responseInput, environmentId });
if (!environmentIdValidation.success) {

View File

@@ -1,6 +1,7 @@
import { NextRequest, userAgent } from "next/server";
import { logger } from "@formbricks/logger";
import { TContactAttributes } from "@formbricks/types/contact-attribute";
import { ZEnvironmentId } from "@formbricks/types/environment";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TJsPersonState } from "@formbricks/types/js";
import { responses } from "@/app/lib/api/response";
@@ -29,15 +30,36 @@ export const POST = withV1ApiWrapper({
const params = await props.params;
try {
const { environmentId } = params;
// Simple validation (faster than Zod for high-frequency endpoint)
if (!environmentId || typeof environmentId !== "string") {
// Basic type check for environmentId
if (typeof params.environmentId !== "string") {
return {
response: responses.badRequestResponse("Environment ID is required", undefined, true),
};
}
const environmentId = params.environmentId.trim();
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
// This catches all invalid formats including:
// - null/undefined passed as string "null" or "undefined"
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
// - Empty or whitespace-only IDs
// - Any other invalid CUID v1 format
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
if (!cuidValidation.success) {
logger.warn(
{
environmentId: params.environmentId,
url: req.url,
validationError: cuidValidation.error.errors[0]?.message,
},
"Invalid CUID v1 format detected"
);
return {
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
};
}
const jsonInput = await req.json();
// Basic input validation without Zod overhead

View File

@@ -26,7 +26,7 @@ const baseProject = {
darkOverlay: false,
environments: [
{
id: "prodenv",
id: "cmi2sra0j000004l73fvh7lhe",
createdAt: new Date(),
updatedAt: new Date(),
type: "production" as TEnvironment["type"],
@@ -34,7 +34,7 @@ const baseProject = {
appSetupCompleted: false,
},
{
id: "devenv",
id: "cmi2srt9q000104l7127e67v7",
createdAt: new Date(),
updatedAt: new Date(),
type: "development" as TEnvironment["type"],
@@ -155,7 +155,7 @@ describe("project lib", () => {
vi.mocked(deleteFilesByEnvironmentId).mockResolvedValue({ ok: true, data: undefined });
const result = await deleteProject("p1");
expect(result).toEqual(baseProject);
expect(deleteFilesByEnvironmentId).toHaveBeenCalledWith("prodenv");
expect(deleteFilesByEnvironmentId).toHaveBeenCalledWith("cmi2sra0j000004l73fvh7lhe");
});
test("logs error if file deletion fails", async () => {

View File

@@ -1,7 +1,11 @@
import { z } from "zod";
export const ZEnvironmentId = z.string().cuid();
export type TEnvironmentId = z.infer<typeof ZEnvironmentId>;
export const ZEnvironment = z.object({
id: z.string().cuid2(),
id: ZEnvironmentId,
createdAt: z.date(),
updatedAt: z.date(),
type: z.enum(["development", "production"]),
@@ -11,12 +15,6 @@ export const ZEnvironment = z.object({
export type TEnvironment = z.infer<typeof ZEnvironment>;
export const ZEnvironmentId = z.object({
id: z.string(),
});
export type TEnvironmentId = z.infer<typeof ZEnvironmentId>;
export const ZEnvironmentUpdateInput = z.object({
type: z.enum(["development", "production"]),
projectId: z.string(),