{"record":{"id":"fb83cd7fad431e08","repo":"docker/compose","slug":"git-subdirectory-escapes-base-directory-s","errorCode":null,"errorMessage":"git subdirectory escapes base directory: %s","messagePattern":"git subdirectory escapes base directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/remote/git.go","lineNumber":166,"sourceCode":"\t\treturn fmt.Errorf(\"git subdirectory path traversal detected: %s\", subDir)\n\t}\n\n\tif len(cleanSubDir) >= 2 && cleanSubDir[1] == ':' {\n\t\treturn fmt.Errorf(\"git subdirectory must be relative, got: %s\", subDir)\n\t}\n\n\ttargetPath := filepath.Join(base, cleanSubDir)\n\tcleanBase := filepath.Clean(base)\n\tcleanTarget := filepath.Clean(targetPath)\n\n\t// Ensure the target starts with the base path\n\trelPath, err := filepath.Rel(cleanBase, cleanTarget)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid git subdirectory path: %w\", err)\n\t}\n\n\tif relPath == \"..\" || strings.HasPrefix(relPath, \"../\") || strings.HasPrefix(relPath, \"..\\\\\") {\n\t\treturn fmt.Errorf(\"git subdirectory escapes base directory: %s\", subDir)\n\t}\n\n\treturn nil\n}\n\nfunc (g gitRemoteLoader) resolveGitRef(ctx context.Context, path string, ref *gitutil.GitRef) error {\n\tif !commitSHA.MatchString(ref.Ref) {\n\t\tcmd := exec.CommandContext(ctx, \"git\", \"ls-remote\", \"--exit-code\", ref.Remote, ref.Ref)\n\t\tcmd.Env = g.gitCommandEnv()\n\t\tout, err := cmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\tif cmd.ProcessState.ExitCode() == 2 {\n\t\t\t\treturn fmt.Errorf(\"repository does not contain ref %s, output: %q: %w\", path, string(out), err)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"failed to access repository at %s:\\n %s\", ref.Remote, out)\n\t\t}\n\t\tif len(out) < 40 {\n\t\t\treturn fmt.Errorf(\"unexpected git command output: %q\", string(out))","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/docker/compose/blob/ddc4b044b62e9f715212ea4143fa830fac76382f/pkg/remote/git.go#L148-L184","documentation":"Final containment check of `validateGitSubDir`: even after cleaning and joining, the resolved target's path relative to the base escapes upward (`..` prefix), so the sub-directory would resolve outside the git checkout. Compose blocks it to keep includes confined to the cloned repo.","triggerScenarios":"Sub-paths like `a/../../escape` where intermediate components rejoin under base per cleaning rules but `filepath.Join` + `Rel` still yields an escaping relative path; symlink-free lexical traversal that the earlier prefix checks didn't catch.","commonSituations":"Maliciously crafted include fragments attempting cache escape; over-clever relative paths that try to reference repos cached next to each other.","solutions":["Simplify the fragment to a direct sub-path within the repository","Treat hitting this error as a red flag if the path came from an untrusted source (CI variable, remote config)"],"exampleFix":"# before\ninclude:\n  - path: git://github.com/org/repo.git#main:sub/../../escape\n\n# after\ninclude:\n  - path: git://github.com/org/repo.git#main:sub","handlingStrategy":"validation","validationCode":"# normalize then assert containment, mirroring the library check\npython3 - <<'EOF'\nimport sys, posixpath\nbase = '/checkout/root'\nfor sub in sys.argv[1:]:\n    rel = posixpath.relpath(posixpath.normpath(posixpath.join(base, sub)), posixpath.normpath(base))\n    if rel == '..' or rel.startswith('../'):\n        sys.exit(f\"fragment escapes base: {sub}\")\nEOF","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat any 'escapes base directory' hit on untrusted input as a security signal","Simplify include fragments; deep relative gymnastics are both fragile and suspicious"],"tags":["compose","git-remote","path-traversal","security"],"backgroundTag":null,"analyzedSha":"ddc4b044b62e9f715212ea4143fa830fac76382f","analyzedAt":"2026-08-15T13:31:42.319Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}