{"record":{"id":"fe678277191e17ee","repo":"golang/go","slug":"file-q-is-outside-working-directory","errorCode":null,"errorMessage":"file %#q is outside working directory","messagePattern":"file %#q is outside working directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cmd/internal/script/state.go","lineNumber":165,"sourceCode":"// originally created.\nfunc (s *State) ExtractFiles(ar *txtar.Archive) error {\n\twd := s.workdir\n\n\t// Add trailing separator to terminate wd.\n\t// This prevents extracting to outside paths which prefix wd,\n\t// e.g. extracting to /home/foobar when wd is /home/foo\n\tif wd == \"\" {\n\t\tpanic(\"s.workdir is unexpectedly empty\")\n\t}\n\tif !os.IsPathSeparator(wd[len(wd)-1]) {\n\t\twd += string(filepath.Separator)\n\t}\n\n\tfor _, f := range ar.Files {\n\t\tname := s.Path(s.ExpandEnv(f.Name, false))\n\n\t\tif !strings.HasPrefix(name, wd) {\n\t\t\treturn fmt.Errorf(\"file %#q is outside working directory\", f.Name)\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(name), 0777); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := os.WriteFile(name, f.Data, 0666); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Getwd returns the directory in which to run the next script command.\nfunc (s *State) Getwd() string { return s.pwd }\n\n// Logf writes output to the script's log without updating its stdout or stderr\n// buffers. (The output log functions as a kind of meta-stderr.)","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/cmd/internal/script/state.go#L147-L183","documentation":"Thrown by State.ExtractFiles (state.go:148-177) as a path-traversal (zip-slip) guard. Each archive file name is resolved under s.workdir; if the resolved path does not have the workdir as a prefix, extraction is refused. This prevents a maliciously or accidentally crafted archive from writing outside the test's temporary working directory.","triggerScenarios":"An archive entry whose name contains '../' (e.g. '../escape.txt'), is absolute ('/etc/foo'), or contains env vars (expanded via ExpandEnv) that resolve to a path outside workdir. The HasPrefix check at state.go:164 fails.","commonSituations":"Hand-written txtar with a leading '../'; a $envvar in the file name expanding to an absolute path; symlink in workdir causing resolution outside the prefix; archive copied from another test with absolute paths.","solutions":["Make all archive file names relative and contained within the workdir (no leading '../' or absolute paths).","Audit any env-var placeholders in file names passed through ExpandEnv to ensure they resolve to relative paths.","Sanitize names with filepath.Clean and reject those that escape before adding to the archive.","If a file legitimately needs to live outside workdir, write it directly with os.WriteFile instead of ExtractFiles."],"exampleFix":"// before — txtar entry escapes workdir\n-- ../outside.txt--\ncontents\n// after\n-- subdir/outside.txt--\ncontents","handlingStrategy":"validation","validationCode":"import (\"os\"; \"path/filepath\"; \"strings\")\n\nfunc archiveNameSafe(wd, name string) bool {\n    full := filepath.Join(wd, filepath.Clean(\"/\"+name)) // force relative\n    wdAbs, _ := filepath.Abs(wd)\n    fullAbs, _ := filepath.Abs(full)\n    return strings.HasPrefix(fullAbs+string(filepath.Separator), wdAbs+string(filepath.Separator))\n}\n\n// Reject before calling ExtractFiles:\nfor _, f := range ar.Files {\n    if !archiveNameSafe(s.workdir, f.Name) {\n        return fmt.Errorf(\"unsafe archive entry %q\", f.Name)\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never put absolute paths or '../' in txtar archive file names.","Audit env-var placeholders in names passed to ExpandEnv — they can resolve outside workdir.","Sanitize external archive inputs with filepath.Clean and a prefix check before extraction."],"tags":["go-script-test","path-traversal","security","extractfiles"],"backgroundTag":null,"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:31:55.035Z"}