golang/go · error

file %#q is outside working directory

Error message

file %#q is outside working directory

What it means

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.

Source

Thrown at src/cmd/internal/script/state.go:165

// originally created.
func (s *State) ExtractFiles(ar *txtar.Archive) error {
	wd := s.workdir

	// Add trailing separator to terminate wd.
	// This prevents extracting to outside paths which prefix wd,
	// e.g. extracting to /home/foobar when wd is /home/foo
	if wd == "" {
		panic("s.workdir is unexpectedly empty")
	}
	if !os.IsPathSeparator(wd[len(wd)-1]) {
		wd += string(filepath.Separator)
	}

	for _, f := range ar.Files {
		name := s.Path(s.ExpandEnv(f.Name, false))

		if !strings.HasPrefix(name, wd) {
			return fmt.Errorf("file %#q is outside working directory", f.Name)
		}

		if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil {
			return err
		}
		if err := os.WriteFile(name, f.Data, 0666); err != nil {
			return err
		}
	}

	return nil
}

// Getwd returns the directory in which to run the next script command.
func (s *State) Getwd() string { return s.pwd }

// Logf writes output to the script's log without updating its stdout or stderr
// buffers. (The output log functions as a kind of meta-stderr.)

View on GitHub (pinned to b6b368adc5)

Solutions

  1. Make all archive file names relative and contained within the workdir (no leading '../' or absolute paths).
  2. Audit any env-var placeholders in file names passed through ExpandEnv to ensure they resolve to relative paths.
  3. Sanitize names with filepath.Clean and reject those that escape before adding to the archive.
  4. If a file legitimately needs to live outside workdir, write it directly with os.WriteFile instead of ExtractFiles.

Example fix

// before — txtar entry escapes workdir
-- ../outside.txt--
contents
// after
-- subdir/outside.txt--
contents
Defensive patterns

Strategy: validation

Validate before calling

import ("os"; "path/filepath"; "strings")

func archiveNameSafe(wd, name string) bool {
    full := filepath.Join(wd, filepath.Clean("/"+name)) // force relative
    wdAbs, _ := filepath.Abs(wd)
    fullAbs, _ := filepath.Abs(full)
    return strings.HasPrefix(fullAbs+string(filepath.Separator), wdAbs+string(filepath.Separator))
}

// Reject before calling ExtractFiles:
for _, f := range ar.Files {
    if !archiveNameSafe(s.workdir, f.Name) {
        return fmt.Errorf("unsafe archive entry %q", f.Name)
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of golang/go@b6b368adc5 (2026-08-12). Data as JSON: /api/errors/fe678277191e17ee. Report an issue: GitHub.