{"record":{"id":"8d58165bb87ad8c3","repo":"alibaba/open-code-review","slug":"rule-file-path-q-escapes-repo-dir-q","errorCode":null,"errorMessage":"rule file path %q escapes repo dir %q","messagePattern":"rule file path %q escapes repo dir %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/rules/system_rules.go","lineNumber":643,"sourceCode":"\tif os.IsNotExist(err) {\n\t\tfmt.Fprintf(os.Stderr, \"[ocr] WARNING: rule file not found: %s\\n\", rule)\n\t} else {\n\t\tfmt.Fprintf(os.Stderr, \"[ocr] WARNING: cannot read rule file %s: %v\\n\", resolved, err)\n\t}\n\treturn nil\n}\n\n// readRuleFileSafe reads and validates a rule file: extension whitelist, 512 KB cap,\n// and symlink resolution. When confineRoot is non-empty, the resolved path must stay\n// inside it. Returns the trimmed content on success.\nfunc readRuleFileSafe(path string, confineRoot string) (string, error) {\n\tresolved, err := filepath.EvalSymlinks(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif confineRoot != \"\" && !pathutil.WithinBase(confineRoot, resolved) {\n\t\treturn \"\", fmt.Errorf(\"rule file path %q escapes repo dir %q\", resolved, confineRoot)\n\t}\n\n\tif !allowedRuleExts[strings.ToLower(filepath.Ext(resolved))] {\n\t\treturn \"\", fmt.Errorf(\"unsupported extension %q, only .md/.txt/.markdown allowed\", filepath.Ext(resolved))\n\t}\n\n\tconst maxSize = 512 * 1024\n\tinfo, err := os.Stat(resolved)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif info.Size() > maxSize {\n\t\treturn \"\", fmt.Errorf(\"file too large (%d bytes, max %d)\", info.Size(), maxSize)\n\t}\n\n\tcontent, err := os.ReadFile(resolved)\n\tif err != nil {\n\t\treturn \"\", err","sourceCodeStart":625,"sourceCodeEnd":661,"githubUrl":"https://github.com/alibaba/open-code-review/blob/5cf97d0d15cbd41b602513c4be3bfec3cee5bf7f/internal/config/rules/system_rules.go#L625-L661","documentation":"readRuleFileSafe resolves a user-supplied rule file path via EvalSymlinks and enforces a confinement policy: if confineRoot is set and the resolved path is not inside it, the read is refused. This blocks a malicious or misconfigured rule reference from exfiltrating files outside the repository (rule content is sent to the LLM).","triggerScenarios":"tryReadRuleFile is called with a path whose symlink-resolved target lies outside the repo dir — e.g. --rule /etc/passwd, --rule ../secrets.md, or a rule file that is a symlink to outside the repo.","commonSituations":"Pointing --rule at a shared rules file in another repo or a home directory; a symlinked file that used to be in-repo but now targets an absolute path elsewhere; running with a repoDir that differs from where rule files live.","solutions":["Place the rule file inside the repository and pass a repo-root-relative path","Replace absolute-path symlinks with real files (or symlinks whose resolved target is inside the repo)","Copy the shared rule file into this repo instead of referencing it across repos","If intentionally external, use --repo to set confineRoot appropriately or keep the rule in the repo root"],"exampleFix":"// before\nocr review --rule /home/me/shared-rules.md\n// after\ncp /home/me/shared-rules.md ./docs/review-rules.md\nocr review --rule docs/review-rules.md","handlingStrategy":"validation","validationCode":"import \"path/filepath\"\nfunc ruleInsideRepo(repoDir, rulePath string) (bool, error) {\n    root, err := filepath.EvalSymlinks(repoDir)\n    if err != nil { return false, err }\n    resolved, err := filepath.EvalSymlinks(rulePath)\n    if err != nil { return false, err }\n    rel, err := filepath.Rel(root, resolved)\n    if err != nil { return false, err }\n    return rel != \"..\" && !strings.HasPrefix(rel, \"..\"+string(filepath.Separator)), nil\n}","typeGuard":null,"tryCatchPattern":"_, err := tryReadRuleFile(rulePath, confineRoot)\nif err != nil && strings.Contains(err.Error(), \"escapes repo dir\") {\n    fmt.Fprintf(os.Stderr, \"move %s inside the repo, or drop the --rule flag\\n\", rulePath)\n    os.Exit(2)\n}","preventionTips":["Keep rule files as regular in-repo files, not cross-repo symlinks","Never pass absolute paths outside the repo to --rule","Run ocr with the repo you intend as confineRoot, not a parent directory","Audit rule files for symlink targets after cloning a foreign repo"],"tags":["go","security","path-traversal","symlink"],"backgroundTag":"path-escapes-base-directory","analyzedSha":"5cf97d0d15cbd41b602513c4be3bfec3cee5bf7f","analyzedAt":"2026-09-02T02:08:09.116Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}