diff --git a/agent/internal/agent/backup.go b/agent/internal/agent/backup.go index 509b350..7469298 100644 --- a/agent/internal/agent/backup.go +++ b/agent/internal/agent/backup.go @@ -957,6 +957,14 @@ func extractTarGz(archivePath, destPath string) error { return fmt.Errorf("invalid tar entry: %s", header.Name) } + parentDir := filepath.Dir(targetPath) + if resolvedParent, err := filepath.EvalSymlinks(parentDir); err == nil { + if !strings.HasPrefix(resolvedParent, filepath.Clean(destPath)+string(os.PathSeparator)) && + resolvedParent != filepath.Clean(destPath) { + return fmt.Errorf("invalid tar entry (symlink traversal): %s", header.Name) + } + } + switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { @@ -976,6 +984,15 @@ func extractTarGz(archivePath, destPath string) error { } outFile.Close() case tar.TypeSymlink: + linkTarget := header.Linkname + if !filepath.IsAbs(linkTarget) { + linkTarget = filepath.Join(filepath.Dir(targetPath), linkTarget) + } + resolvedLink := filepath.Clean(linkTarget) + if !strings.HasPrefix(resolvedLink, filepath.Clean(destPath)+string(os.PathSeparator)) && + resolvedLink != filepath.Clean(destPath) { + return fmt.Errorf("invalid symlink target: %s -> %s", header.Name, header.Linkname) + } if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { return fmt.Errorf("failed to create parent directory: %w", err) } diff --git a/web/actions/projects.ts b/web/actions/projects.ts index 59fc8c1..862f069 100644 --- a/web/actions/projects.ts +++ b/web/actions/projects.ts @@ -41,9 +41,6 @@ import { startMigration } from "./migrations"; import { inngest } from "@/lib/inngest/client"; function isValidImageReferencePart(reference: string): boolean { - // Allow only characters that are valid in Docker tags/digests and avoid path traversal. - // Tags: letters, digits, underscores, periods and dashes; Digests: "algorithm:hex". - // This regex is intentionally conservative. const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/; const digestPattern = /^[A-Za-z0-9_+.-]+:[0-9a-fA-F]{32,256}$/; @@ -54,6 +51,11 @@ function isValidImageReferencePart(reference: string): boolean { ); } +function isValidImageNamePart(part: string): boolean { + const segmentPattern = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/; + return part.split("/").every((segment) => segmentPattern.test(segment)); +} + function parseImageReference(image: string): { registry: string; namespace: string; @@ -117,6 +119,13 @@ export async function validateDockerImage( }; } + if (!isValidImageNamePart(namespace) || !isValidImageNamePart(repository)) { + return { + valid: false, + error: "Invalid image name", + }; + } + if (registry === "docker.io") { const repoPath = namespace === "library" ? repository : `${namespace}/${repository}`; diff --git a/web/app/(dashboard)/dashboard/projects/[slug]/[env]/import-compose/import-compose-form.tsx b/web/app/(dashboard)/dashboard/projects/[slug]/[env]/import-compose/import-compose-form.tsx index c583a7d..54d25af 100644 --- a/web/app/(dashboard)/dashboard/projects/[slug]/[env]/import-compose/import-compose-form.tsx +++ b/web/app/(dashboard)/dashboard/projects/[slug]/[env]/import-compose/import-compose-form.tsx @@ -243,8 +243,8 @@ services: Error - {errors.map((err, i) => ( -
{err.message}
+ {errors.map((err) => ( +
{err.message}
))}
@@ -335,8 +335,8 @@ services: