{"record":{"id":"7ed122a506124ba1","repo":"lima-vm/lima","slug":"symlink-target-q-in-q-escapes-the-repository","errorCode":null,"errorMessage":"symlink target %#q in %#q escapes the repository","messagePattern":"symlink target %#q in %#q escapes the repository","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/limatmpl/github.go","lineNumber":200,"sourceCode":"\n\t// A symlink must be a single line (without trailing newline), no spaces, no colons\n\tif !(content == \"\" || strings.ContainsAny(content, \"\\n :\")) {\n\t\t// symlink is relative to the directory of filePath, and must stay within the repo\n\t\tfilePath, err = symlinkTarget(filePath, content)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn githubUserContentURL(org, repo, branch, filePath), nil\n}\n\n// symlinkTarget resolves a symlink target relative to the directory of filePath.\n// It returns an error if the target escapes the repository root, so a symlink\n// cannot redirect the fetch to a different repository via `../` segments.\nfunc symlinkTarget(filePath, target string) (string, error) {\n\tresolved := path.Join(path.Dir(filePath), target)\n\tif escapesRepo(resolved) {\n\t\treturn \"\", fmt.Errorf(\"symlink target %#q in %#q escapes the repository\", target, filePath)\n\t}\n\treturn resolved, nil\n}\n\n// escapesRepo reports whether the cleaned path climbs above the repository root.\nfunc escapesRepo(p string) bool {\n\tp = path.Clean(p)\n\treturn p == \"..\" || strings.HasPrefix(p, \"../\")\n}\n\n// resolveGitHubRedirect checks if a file at the given path is a github: URL to another file within the same repo.\n// Returns the URL, or an error if the file doesn't exist, or doesn't start with github:ORG.\nfunc resolveGitHubRedirect(ctx context.Context, org, repo, defaultBranch, filePath, origBranch string) (string, error) {\n\t// Refetch the filepath from the defaultBranch\n\tresp, err := getGitHubUserContent(ctx, org, repo, defaultBranch, filePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch file: %w\", err)\n\t}","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/lima-vm/lima/blob/dd909d0973cd84fa35f9e1693181b4585ea616c1/pkg/limatmpl/github.go#L182-L218","documentation":"symlinkTarget resolves a file-symlink target relative to the directory of the fetched file and refuses targets whose cleaned path climbs above the repository root (paths starting with ../ after path.Join/Clean). This security check prevents a template's symlink file from redirecting the fetch to a different repository or host path via .. segments.","triggerScenarios":"transformGitHubURL -> resolveGitHubSymlink finds the fetched .yaml content is a single-line path (no newline, space, or colon) and calls symlinkTarget; the target resolves to a path like ../../other/repo/file.yaml that escapesRepo() flags.","commonSituations":"A template repo containing a symlink file that points outside its own tree (e.g. ../shared/config.yaml); content-antisymmetry mistakes where a one-line value happens to look like a path; a compromised or misconfigured upstream template attempting path traversal.","solutions":["Fix the upstream symlink file to use an in-repo relative target (no leading ../ beyond the repo root)","If the file was never intended to be a symlink, reformat it so it contains a newline, space, or colon (e.g. make it a comment-bearing YAML) so it is not treated as a symlink path","Reference the actual target file directly in your github: URL instead of going through the symlink","Fork the template repo and correct the symlink target"],"exampleFix":"// symlink file content, before\n../../../etc/passwd\n// after\nconfigs/default.yaml","handlingStrategy":"validation","validationCode":"func safeTarget(dir, target string) bool {\n    resolved := path.Clean(path.Join(dir, target))\n    return resolved != \"..\" && !strings.HasPrefix(resolved, \"../\")\n}\n// check before fetching a template whose symlink content you control\nif !safeTarget(\"templates\", contents) {\n    return errors.New(\"symlink target escapes repo\")\n}","typeGuard":"func escapesRepo(p string) bool {\n    p = path.Clean(p)\n    return p == \"..\" || strings.HasPrefix(p, \"../\")\n}","tryCatchPattern":"url, err := transformGitHubURL(ctx, ref)\nif err != nil && strings.Contains(err.Error(), \"escapes the repository\") {\n    return fmt.Errorf(\"refusing template: symlink in %s points outside its repo; reference the target file directly\", ref)\n}","preventionTips":["Keep symlink files pointing only at in-repo relative paths","Never author symlink-style one-line files with ../ segments","Reference the real target file directly instead of a traversing symlink","Audit template repos' one-line files, since any single line without newline/space/colon is treated as a symlink"],"tags":["path-traversal","security","symlink"],"backgroundTag":"path-escapes-repository","analyzedSha":"dd909d0973cd84fa35f9e1693181b4585ea616c1","analyzedAt":"2026-09-01T14:24:59.842Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}