{"record":{"id":"7c1fc67c0403ef46","repo":"cilium/cilium","slug":"refusing-to-write-q-outside-of-the-sysdump-direct","errorCode":null,"errorMessage":"refusing to write %q outside of the sysdump directory","messagePattern":"refusing to write %q outside of the sysdump directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cilium-cli/sysdump/sysdump.go","lineNumber":426,"sourceCode":"\n// replaceTimestamp can be used to replace the special timestamp placeholder in file and directory names.\nfunc (c *Collector) replaceTimestamp(f string) string {\n\treturn strings.ReplaceAll(f, timestampPlaceholderFileName, c.startTime.Format(timeFormat))\n}\n\n// AbsoluteTempPath returns the absolute path where to store the specified filename temporarily.\nfunc (c *Collector) AbsoluteTempPath(f string) string {\n\treturn path.Join(c.sysdumpDir, c.replaceTimestamp(f))\n}\n\nfunc (c *Collector) WithFileSink(filename string, fn func(io.Writer) error) error {\n\tpath := c.AbsoluteTempPath(filename)\n\t// filename can be derived from data collected inside a target pod (for\n\t// example the CNI config file names that SubmitCniConflistSubtask reads\n\t// from `ls` output), so reject anything that resolves outside the sysdump\n\t// directory before opening it.\n\tif !strings.HasPrefix(filepath.Clean(path), filepath.Clean(c.sysdumpDir)+string(os.PathSeparator)) {\n\t\treturn fmt.Errorf(\"refusing to write %q outside of the sysdump directory\", filename)\n\t}\n\tfile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn errors.Join(\n\t\tfn(file),\n\t\tfile.Close(),\n\t)\n}\n\n// WriteYAML writes a kubernetes object to a file as YAML.\nfunc (c *Collector) WriteYAML(filename string, o runtime.Object) error {\n\treturn c.WithFileSink(filename, func(w io.Writer) error {\n\t\treturn writeYAML(o, w)\n\t})\n}","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/cilium/cilium/blob/ac7b90affa4baf0642e6685319d56907b3a73a6d/cilium-cli/sysdump/sysdump.go#L408-L444","documentation":"WithFileSink is the collector's single choke point for opening files inside the sysdump directory. Because filenames can be derived from remote data (e.g. CNI config file names read from `ls` output inside a target pod), the collector rejects any filename whose cleaned absolute path does not resolve under c.sysdumpDir before opening it. This is a path-traversal guard, not a filesystem error — nothing was written.","triggerScenarios":"Calling WithFileSink (directly or via WriteYAML/WriteString/WriteTable/WriteEventTable, or the anonymous log sink, or submitKVStoreTasks) with a filename that, after timestamp replacement and path.Join/filepath.Clean, escapes the sysdump directory — e.g. '../foo.yaml', an absolute path like '/etc/cni/…', or a pod-supplied name containing '..'.","commonSituations":"Subtasks that write files named from pod exec output (CNI conflist collection, command output used as filename), misconfigured custom sysdump tasks using absolute paths, or a sysdumpDir itself containing '..' or a trailing-symlink edge that makes Clean resolve differently.","solutions":["Sanitize the filename before passing it: strip path separators and '..' segments, keeping only filepath.Base(filename).","Never pass absolute paths or data straight from pod output; join remote-derived names onto a fixed prefix like 'pods/<ns>/<pod>/'.","Check what filename was logged in the error and confirm whether it legitimately should live outside the sysdump dir — if so, that write does not belong in this API.","If the guard false-positives because sysdumpDir itself is unclean (symlink, trailing slash, '..'), ensure the collector is created with an absolute, filepath.EvalSymlinks-resolved directory."],"exampleFix":"// before\nc.WriteYAML(cniConfigName, conf) // cniConfigName = \"../../etc/cni/10-flannel.conf\" from pod ls output\n\n// after\nsafe := filepath.Base(cniConfigName) // \"10-flannel.conf\"\nc.WriteYAML(filepath.Join(\"cni\", safe), conf)","handlingStrategy":"validation","validationCode":"func safeRelName(name string) string {\n\tbase := filepath.Base(strings.ReplaceAll(name, \"\\\\\", \"/\"))\n\tif base == \".\" || base == \"..\" || base == string(filepath.Separator) {\n\t\treturn \"\"\n\t}\n\treturn base\n}\n// before calling: filename := filepath.Join(\"cni\", safeRelName(podDerivedName)); if filename == \"\" { skip }","typeGuard":"func isInsideSysdump(path, sysdumpDir string) bool {\n\trel, err := filepath.Rel(filepath.Clean(sysdumpDir), filepath.Clean(path))\n\treturn err == nil && rel != \"..\" && !strings.HasPrefix(rel, \"..\"+string(os.PathSeparator))\n}","tryCatchPattern":"if err := collector.WriteYAML(filename, obj); err != nil {\n\tvar pathErr *os.PathError\n\tif strings.Contains(err.Error(), \"refusing to write\") {\n\t\tlog.Printf(\"skipping unsafe filename %q\", filename)\n\t} else if errors.As(err, &pathErr) {\n\t\tlog.Printf(\"filesystem error writing sysdump file: %v\", pathErr)\n\t}\n}","preventionTips":["Never use pod-exec or API output verbatim as a filename; always reduce it with filepath.Base and drop '..' segments.","Keep all sysdump filenames as fixed constants or fixed prefixes plus sanitized basenames.","Create the collector with an absolute, symlink-resolved (filepath.EvalSymlinks) sysdump directory."],"tags":["go","path-traversal","security","sysdump","cilium-cli"],"backgroundTag":"path-escapes-output-directory","analyzedSha":"ac7b90affa4baf0642e6685319d56907b3a73a6d","analyzedAt":"2026-08-31T18:27:15.868Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}