diff --git a/backend/e2e-test/routes/v3/secret-rotations.spec.ts b/backend/e2e-test/routes/v3/secret-rotations.spec.ts index 00fc2df9231..4e73196d610 100644 --- a/backend/e2e-test/routes/v3/secret-rotations.spec.ts +++ b/backend/e2e-test/routes/v3/secret-rotations.spec.ts @@ -32,7 +32,7 @@ const formatSqlUsername = (username: string) => `${username}_${uuidv4().slice(0, const getSecretValue = async (secretKey: string) => { const passwordSecret = await testServer.inject({ - url: `/api/v3/secrets/raw/${secretKey}`, + url: `/api/v3/secrets/raw/${encodeURIComponent(secretKey)}`, method: "GET", query: { workspaceId: seedData1.projectV3.id, diff --git a/backend/e2e-test/routes/v4/secrets.spec.ts b/backend/e2e-test/routes/v4/secrets.spec.ts index 0276cba7bc2..267bd1e9ef7 100644 --- a/backend/e2e-test/routes/v4/secrets.spec.ts +++ b/backend/e2e-test/routes/v4/secrets.spec.ts @@ -154,6 +154,23 @@ describe.each([{ auth: AuthMode.JWT }, { auth: AuthMode.IDENTITY_ACCESS_TOKEN }] "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4gU2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uCg==", comment: "" } + }, + { + path: "/", + secret: { + // Encoding the key simulates the actual HTTP request format + key: encodeURIComponent("path/to/secret"), + value: "slash-value", + comment: "Testing forward slashes in keys" + } + }, + { + path: "/nested1/nested2/folder", + secret: { + key: encodeURIComponent("another/key/with/slashes"), + value: "nested-slash-value", + comment: "Testing slashes in nested paths" + } } ]; diff --git a/backend/src/server/lib/schemas.ts b/backend/src/server/lib/schemas.ts index 035e645f489..4496aa5ecc4 100644 --- a/backend/src/server/lib/schemas.ts +++ b/backend/src/server/lib/schemas.ts @@ -43,6 +43,6 @@ export const GenericResourceNameSchema = z export const BaseSecretNameSchema = z.string().trim().min(1); export const SecretNameSchema = BaseSecretNameSchema.refine( - (el) => !el.includes(":") && !el.includes("/"), - "Secret name cannot contain colon or forward slash." + (el) => !el.includes(":"), + "Secret name cannot contain colon" ); diff --git a/backend/src/server/routes/v4/secret-router.ts b/backend/src/server/routes/v4/secret-router.ts index 9be378af9a3..cd0ff62fb63 100644 --- a/backend/src/server/routes/v4/secret-router.ts +++ b/backend/src/server/routes/v4/secret-router.ts @@ -269,7 +269,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:secretName", + url: "/*", config: { rateLimit: secretsLimit }, @@ -284,7 +284,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET.secretName) + "*": z.string().trim().describe(RAW_SECRETS.GET.secretName) }), querystring: z.object({ projectId: z.string().trim().describe(RAW_SECRETS.GET.projectId), @@ -336,7 +336,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { projectId, viewSecretValue: req.query.viewSecretValue, path: secretPath, - secretName: req.params.secretName, + secretName: req.params["*"], type: req.query.type, includeImports: req.query.includeImports, version: req.query.version @@ -351,7 +351,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment, secretPath: req.query.secretPath, secretId: secret.id, - secretKey: req.params.secretName, + secretKey: req.params["*"], secretVersion: secret.version, secretMetadata: secret.secretMetadata?.map((meta) => ({ key: meta.key, @@ -383,7 +383,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", - url: "/:secretName", + url: "/*", config: { rateLimit: secretsLimit }, @@ -398,7 +398,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) + "*": SecretNameSchema.describe(RAW_SECRETS.CREATE.secretName) }), body: z.object({ projectId: z.string().trim().describe(RAW_SECRETS.CREATE.projectId), @@ -449,7 +449,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, projectId: req.body.projectId, secretPath: req.body.secretPath, - secretName: req.params.secretName, + secretName: req.params["*"], type: req.body.type, secretValue: req.body.secretValue, skipMultilineEncoding: req.body.skipMultilineEncoding, @@ -471,7 +471,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretApprovalRequestSlug: secretOperation.approval.slug, secretPath: req.body.secretPath, environment: req.body.environment, - secretKey: req.params.secretName, + secretKey: req.params["*"], eventType: SecretApprovalEvent.Create } } @@ -490,7 +490,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.body.environment, secretPath: req.body.secretPath, secretId: secret.id, - secretKey: req.params.secretName, + secretKey: req.params["*"], secretVersion: secret.version, secretMetadata: req.body.secretMetadata?.map((meta) => ({ key: meta.key, @@ -522,7 +522,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "PATCH", - url: "/:secretName", + url: "/*", config: { rateLimit: secretsLimit }, @@ -537,7 +537,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) + "*": BaseSecretNameSchema.describe(RAW_SECRETS.UPDATE.secretName) }), body: z.object({ projectId: z.string().trim().describe(RAW_SECRETS.UPDATE.projectId), @@ -594,7 +594,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.body.environment, projectId: req.body.projectId, secretPath: req.body.secretPath, - secretName: req.params.secretName, + secretName: req.params["*"], type: req.body.type, secretValue: req.body.secretValue, skipMultilineEncoding: req.body.skipMultilineEncoding, @@ -620,7 +620,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretApprovalRequestSlug: secretOperation.approval.slug, secretPath: req.body.secretPath, environment: req.body.environment, - secretKey: req.params.secretName, + secretKey: req.params["*"], eventType: SecretApprovalEvent.Update } } @@ -639,7 +639,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.body.environment, secretPath: req.body.secretPath, secretId: secret.id, - secretKey: req.params.secretName, + secretKey: req.params["*"], secretVersion: secret.version, secretMetadata: req.body.secretMetadata?.map((meta) => ({ key: meta.key, @@ -670,7 +670,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "DELETE", - url: "/:secretName", + url: "/*", config: { rateLimit: secretsLimit }, @@ -685,7 +685,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) + "*": z.string().min(1).describe(RAW_SECRETS.DELETE.secretName) }), body: z.object({ projectId: z.string().trim().describe(RAW_SECRETS.DELETE.projectId), @@ -719,7 +719,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.body.environment, projectId: req.body.projectId, secretPath: req.body.secretPath, - secretName: req.params.secretName, + secretName: req.params["*"], type: req.body.type }); if (secretOperation.type === SecretProtectionType.Approval) { @@ -734,7 +734,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretApprovalRequestSlug: secretOperation.approval.slug, secretPath: req.body.secretPath, environment: req.body.environment, - secretKey: req.params.secretName, + secretKey: req.params["*"], eventType: SecretApprovalEvent.Delete } } @@ -754,7 +754,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.body.environment, secretPath: req.body.secretPath, secretId: secret.id, - secretKey: req.params.secretName, + secretKey: req.params["*"], secretVersion: secret.version } } @@ -1273,7 +1273,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:secretName/reference-dependency-tree", + url: "/*/reference-dependency-tree", config: { rateLimit: secretsLimit }, @@ -1288,7 +1288,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) + "*": z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) }), querystring: z.object({ projectId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.projectId), @@ -1308,7 +1308,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretName } = req.params; + const { secretName } = req.params["*"], const { secretPath, environment, projectId } = req.query; const { tree } = await server.services.secret.getSecretReferenceDependencyTree({ @@ -1328,7 +1328,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", - url: "/:secretName/secret-reference-tree", + url: "/*/secret-reference-tree", config: { rateLimit: secretsLimit }, @@ -1343,7 +1343,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - secretName: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) + "*": z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.secretName) }), querystring: z.object({ projectId: z.string().trim().describe(RAW_SECRETS.GET_REFERENCE_TREE.projectId), @@ -1364,7 +1364,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { secretName } = req.params; + const { secretName } = req.params["*"], const { secretPath, environment, projectId } = req.query; const { tree, value, secret } = await server.services.secret.getSecretReferenceTree({ actorId: req.permission.id, diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 48b18a06769..6325cc2a405 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -40,7 +40,7 @@ export const useCreateSecretV3 = ({ skipMultilineEncoding, tagIds }) => { - const { data } = await apiRequest.post(`/api/v4/secrets/${secretKey}`, { + const { data } = await apiRequest.post(`/api/v4/secrets/${encodeURIComponent(secretKey)}`, { secretPath, type, environment, @@ -106,7 +106,7 @@ export const useUpdateSecretV3 = ({ skipMultilineEncoding, secretMetadata }) => { - const { data } = await apiRequest.patch(`/api/v4/secrets/${secretKey}`, { + const { data } = await apiRequest.patch(`/api/v4/secrets/${encodeURIComponent(secretKey)}`, { projectId, environment, type, @@ -163,7 +163,7 @@ export const useDeleteSecretV3 = ({ return useMutation({ mutationFn: async ({ secretPath = "/", type, environment, projectId, secretKey, secretId }) => { - const { data } = await apiRequest.delete(`/api/v4/secrets/${secretKey}`, { + const { data } = await apiRequest.delete(`/api/v4/secrets/${encodeURIComponent(secretKey)}`, { data: { projectId, environment, @@ -443,7 +443,7 @@ export const useMoveSecrets = ({ }; export const createSecret = async (dto: TCreateSecretsV3DTO) => { - const { data } = await apiRequest.post(`/api/v4/secrets/${dto.secretKey}`, dto); + const { data } = await apiRequest.post(`/api/v4/secrets/${encodeURIComponent(dto.secretKey)}`, dto); return data; }; diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 2855b6614cb..4f7a7c5ddb3 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -303,7 +303,7 @@ export const useGetSecretAccessList = (dto: TGetSecretAccessListDTO) => groups: SecretAccessListEntry[]; identities: SecretAccessListEntry[]; users: SecretAccessListEntry[]; - }>(`/api/v1/secrets/${dto.secretKey}/access-list`, { + }>(`/api/v1/secrets/${encodeURIComponent(dto.secretKey)}/access-list`, { params: { projectId: dto.projectId, environment: dto.environment, @@ -322,7 +322,7 @@ const fetchSecretReferenceTree = async ({ environmentSlug }: TGetSecretReferenceTreeDTO) => { const { data } = await apiRequest.get<{ tree: TSecretReferenceTraceNode; value: string }>( - `/api/v4/secrets/${secretKey}/secret-reference-tree`, + `/api/v4/secrets/${encodeURIComponent(secretKey)}/secret-reference-tree`, { params: { secretPath,