diff --git a/packages/server/src/db/schema/registry.ts b/packages/server/src/db/schema/registry.ts index ee9ca662a..68db88f80 100644 --- a/packages/server/src/db/schema/registry.ts +++ b/packages/server/src/db/schema/registry.ts @@ -44,11 +44,22 @@ export const registryRelations = relations(registry, ({ many }) => ({ }), })); +// Registry URLs must be hostname[:port] only — no shell metacharacters +// Empty string is allowed (means default/Docker Hub registry) +const registryUrlSchema = z + .string() + .refine( + (val) => + val === "" || + /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(val), + "Registry URL must be a valid hostname or hostname:port (e.g. registry.example.com or localhost:5000)", + ); + const createSchema = createInsertSchema(registry, { registryName: z.string().min(1), username: z.string().min(1), password: z.string().min(1), - registryUrl: z.string(), + registryUrl: registryUrlSchema, organizationId: z.string().min(1), registryId: z.string().min(1), registryType: z.enum(["cloud"]), @@ -61,7 +72,7 @@ export const apiCreateRegistry = createSchema registryName: z.string().min(1), username: z.string().min(1), password: z.string().min(1), - registryUrl: z.string(), + registryUrl: registryUrlSchema, registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), }) @@ -74,7 +85,7 @@ export const apiTestRegistry = createSchema.pick({}).extend({ registryName: z.string().optional(), username: z.string().min(1), password: z.string().min(1), - registryUrl: z.string(), + registryUrl: registryUrlSchema, registryType: z.enum(["cloud"]), imagePrefix: z.string().nullable().optional(), serverId: z.string().optional(), diff --git a/packages/server/src/services/registry.ts b/packages/server/src/services/registry.ts index ad18e0b66..65ba80921 100644 --- a/packages/server/src/services/registry.ts +++ b/packages/server/src/services/registry.ts @@ -85,7 +85,7 @@ export const removeRegistry = async (registryId: string) => { } if (!IS_CLOUD) { - await execAsync(`docker logout ${response.registryUrl}`); + await execAsync(`docker logout ${shEscape(response.registryUrl)}`); } return response;