diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx index a4fab46d9..81a8ca918 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,7 @@ const BitbucketProviderSchema = z.object({ slug: z.string().optional(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().optional(), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx index c149a4ba7..c66a0e43f 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx @@ -6,6 +6,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -41,7 +42,7 @@ const GitProviderSchema = z.object({ repositoryURL: z.string().min(1, { message: "Repository URL is required", }), - branch: z.string().min(1, "Branch required"), + branch: z.string().min(1, "Branch required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx index 02cae2c4a..25a4716d5 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitea-provider.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -72,7 +73,7 @@ const GiteaProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).default([]), enableSubmodules: z.boolean().optional(), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx index 6bce2d243..e3571dcdd 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-github-provider.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GithubIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -55,7 +56,7 @@ const GithubProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), triggerType: z.enum(["push", "tag"]).default("push"), diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx index b49a1658f..dad24281e 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-gitlab-provider.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -58,7 +59,7 @@ const GitlabProviderSchema = z.object({ id: z.number().nullable(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx index 3e099251e..677030879 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-bitbucket-provider-compose.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,7 @@ const BitbucketProviderSchema = z.object({ slug: z.string().optional(), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx index 7878225a9..a6d2f93df 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-git-provider-compose.tsx @@ -6,6 +6,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitIcon } from "@/components/icons/data-tools-icons"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -41,7 +42,7 @@ const GitProviderSchema = z.object({ repositoryURL: z.string().min(1, { message: "Repository URL is required", }), - branch: z.string().min(1, "Branch required"), + branch: z.string().min(1, "Branch required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), sshKey: z.string().optional(), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx index a7277f406..7e931d82f 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitea-provider-compose.tsx @@ -5,6 +5,7 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -57,7 +58,7 @@ const GiteaProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), giteaId: z.string().min(1, "Gitea Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx index 827ce1a8a..10075fb5c 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-github-provider-compose.tsx @@ -1,3 +1,4 @@ +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react"; import Link from "next/link"; @@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({ owner: z.string().min(1, "Owner is required"), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z + .string() + .min(1, "Branch is required") + .regex(VALID_BRANCH_REGEX, "Invalid branch name"), githubId: z.string().min(1, "Github Provider is required"), watchPaths: z.array(z.string()).optional(), triggerType: z.enum(["push", "tag"]).default("push"), diff --git a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx index 63de87d8f..d58469db0 100644 --- a/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/generic/save-gitlab-provider-compose.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; @@ -58,7 +59,7 @@ const GitlabProviderSchema = z.object({ gitlabPathNamespace: z.string().min(1), }) .required(), - branch: z.string().min(1, "Branch is required"), + branch: z.string().min(1, "Branch is required").regex(VALID_BRANCH_REGEX, "Invalid branch name"), gitlabId: z.string().min(1, "Gitlab Provider is required"), watchPaths: z.array(z.string()).optional(), enableSubmodules: z.boolean().default(false), diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index a7067f63f..59dfd3716 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -1,3 +1,4 @@ +import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation"; import { relations } from "drizzle-orm"; import { bigint, @@ -432,17 +433,22 @@ export const apiSaveBuildType = createSchema .required() .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true })); +const branchField = z + .string() + .min(1) + .regex(VALID_BRANCH_REGEX, "Invalid branch name"); + export const apiSaveGithubProvider = createSchema .pick({ applicationId: true, repository: true, - branch: true, owner: true, buildPath: true, githubId: true, }) .required() .extend({ + branch: branchField, triggerType: z.enum(["push", "tag"]).default("push"), }) .required() @@ -451,7 +457,6 @@ export const apiSaveGithubProvider = createSchema export const apiSaveGitlabProvider = createSchema .pick({ applicationId: true, - gitlabBranch: true, gitlabBuildPath: true, gitlabOwner: true, gitlabRepository: true, @@ -460,11 +465,11 @@ export const apiSaveGitlabProvider = createSchema gitlabPathNamespace: true, }) .required() + .extend({ gitlabBranch: branchField }) .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveBitbucketProvider = createSchema .pick({ - bitbucketBranch: true, bitbucketBuildPath: true, bitbucketOwner: true, bitbucketRepository: true, @@ -473,18 +478,19 @@ export const apiSaveBitbucketProvider = createSchema applicationId: true, }) .required() + .extend({ bitbucketBranch: branchField }) .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveGiteaProvider = createSchema .pick({ applicationId: true, - giteaBranch: true, giteaBuildPath: true, giteaOwner: true, giteaRepository: true, giteaId: true, }) .required() + .extend({ giteaBranch: branchField }) .merge(createSchema.pick({ enableSubmodules: true, watchPaths: true })); export const apiSaveDockerProvider = createSchema @@ -499,7 +505,6 @@ export const apiSaveDockerProvider = createSchema export const apiSaveGitProvider = createSchema .pick({ - customGitBranch: true, applicationId: true, customGitBuildPath: true, customGitUrl: true, @@ -507,6 +512,7 @@ export const apiSaveGitProvider = createSchema enableSubmodules: true, }) .required() + .extend({ customGitBranch: branchField }) .merge( createSchema.pick({ customGitSSHKeyId: true, diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0028fc65c..bb113c73d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup"; export * from "./utils/notifications/dokploy-restart"; export * from "./utils/notifications/server-threshold"; export * from "./utils/notifications/utils"; +export * from "./utils/git-branch-validation"; export * from "./utils/process/execAsync"; export * from "./utils/process/spawnAsync"; export * from "./utils/providers/bitbucket"; diff --git a/packages/server/src/utils/git-branch-validation.ts b/packages/server/src/utils/git-branch-validation.ts new file mode 100644 index 000000000..71451390d --- /dev/null +++ b/packages/server/src/utils/git-branch-validation.ts @@ -0,0 +1,3 @@ +// Valid git branch names per git-check-ref-format rules. +// Rejects shell metacharacters that would enable command injection. +export const VALID_BRANCH_REGEX = /^[a-zA-Z0-9._\-/]+$/;