diff --git a/backend/src/lib/validator/index.ts b/backend/src/lib/validator/index.ts index 4340d0210..68cec8f4b 100644 --- a/backend/src/lib/validator/index.ts +++ b/backend/src/lib/validator/index.ts @@ -1,2 +1,3 @@ export { isDisposableEmail } from "./validate-email"; +export { isValidFolderName, isValidSecretPath } from "./validate-folder-name"; export { blockLocalAndPrivateIpAddresses } from "./validate-url"; diff --git a/backend/src/lib/validator/validate-folder-name.ts b/backend/src/lib/validator/validate-folder-name.ts new file mode 100644 index 000000000..1fce780f0 --- /dev/null +++ b/backend/src/lib/validator/validate-folder-name.ts @@ -0,0 +1,8 @@ +// regex to allow only alphanumeric, dash, underscore +export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name); + +export const isValidSecretPath = (path: string) => + path + .split("/") + .filter((el) => el.length) + .every((name) => isValidFolderName(name)); diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index 21fc1bd27..088afec47 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -4,6 +4,7 @@ import { SecretFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { FOLDERS } from "@app/lib/api-docs"; import { prefixWithSlash, removeTrailingSlash } from "@app/lib/fn"; +import { isValidFolderName } from "@app/lib/validator"; import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -25,7 +26,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => body: z.object({ workspaceId: z.string().trim().describe(FOLDERS.CREATE.workspaceId), environment: z.string().trim().describe(FOLDERS.CREATE.environment), - name: z.string().trim().describe(FOLDERS.CREATE.name), + name: z + .string() + .trim() + .describe(FOLDERS.CREATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), path: z .string() .trim() @@ -97,7 +104,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => body: z.object({ workspaceId: z.string().trim().describe(FOLDERS.UPDATE.workspaceId), environment: z.string().trim().describe(FOLDERS.UPDATE.environment), - name: z.string().trim().describe(FOLDERS.UPDATE.name), + name: z + .string() + .trim() + .describe(FOLDERS.UPDATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), path: z .string() .trim() @@ -170,7 +183,13 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .object({ id: z.string().describe(FOLDERS.UPDATE.folderId), environment: z.string().trim().describe(FOLDERS.UPDATE.environment), - name: z.string().trim().describe(FOLDERS.UPDATE.name), + name: z + .string() + .trim() + .describe(FOLDERS.UPDATE.name) + .refine((name) => isValidFolderName(name), { + message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed." + }), path: z .string() .trim() diff --git a/backend/src/services/secret-folder/secret-folder-dal.ts b/backend/src/services/secret-folder/secret-folder-dal.ts index a4e6aca5a..7b25ad4ed 100644 --- a/backend/src/services/secret-folder/secret-folder-dal.ts +++ b/backend/src/services/secret-folder/secret-folder-dal.ts @@ -6,6 +6,7 @@ import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { groupBy, removeTrailingSlash } from "@app/lib/fn"; import { ormify, selectAllTableCols } from "@app/lib/knex"; import { OrderByDirection } from "@app/lib/types"; +import { isValidSecretPath } from "@app/lib/validator"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types"; @@ -214,6 +215,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { const secretFolderOrm = ormify(db, TableName.SecretFolder); const findBySecretPath = async (projectId: string, environment: string, path: string, tx?: Knex) => { + const isValidPath = isValidSecretPath(path); + if (!isValidPath) + throw new BadRequestError({ + message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." + }); + try { const folder = await sqlFindFolderByPathQuery( tx || db.replicaNode(), @@ -236,6 +243,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { // finds folders by path for multiple envs const findBySecretPathMultiEnv = async (projectId: string, environments: string[], path: string, tx?: Knex) => { + const isValidPath = isValidSecretPath(path); + if (!isValidPath) + throw new BadRequestError({ + message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." + }); + try { const pathDepth = removeTrailingSlash(path).split("/").filter(Boolean).length + 1; @@ -267,6 +280,12 @@ export const secretFolderDALFactory = (db: TDbClient) => { // even if its the original given /path1/path2 // it will stop automatically at /path2 const findClosestFolder = async (projectId: string, environment: string, path: string, tx?: Knex) => { + const isValidPath = isValidSecretPath(path); + if (!isValidPath) + throw new BadRequestError({ + message: "Invalid secret path. Only alphanumeric characters, dashes, and underscores are allowed." + }); + try { const folder = await sqlFindFolderByPathQuery( tx || db.replicaNode(), diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 28fd1c5bb..f1042d20c 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -10,9 +10,9 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; -const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g; +const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g; // akhilmhdh: JS regex with global save state in .test -const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([^}]+)}/; +const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([a-zA-Z0-9-_.]+)}/; export const shouldUseSecretV2Bridge = (version: number) => version === 3; diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 4ba089c46..6818b0c37 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge"; import { useToggle } from "@app/hooks"; -const REGEX = /(\${([^}]+)})/g; +const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g; const replaceContentWithDot = (str: string) => { let finalStr = ""; for (let i = 0; i < str.length; i += 1) {