Repository: go
Issue: os: safer file open functions
Labels: Proposal,Proposal-Accepted

Please see the updated proposal in https://github.com/golang/go/issues/67002#issuecomment-2460584052

---

Directory traversal vulnerabilities are a common class of vulnerability, in which an attacker tricks a program into opening a file that it did not intend. These attacks often take the form of providing a relative pathname such as `"../../../etc/passwd"`, which results in access outside an intended location. [CVE-2024-3400](https://nvd.nist.gov/vuln/detail/CVE-2024-3400) is a recent, real-world example of directory traversal leading to an actively exploited remote code execution vulnerability.

A related, but less commonly exploited, class of vulnerability involves unintended symlink traversal, in which an attacker creates a symbolic link in the filesystem and manipulates the target into following it.

I propose adding several new functions to the os package to aid in safely opening files with untrusted filename components and defending against symlink traversal.

----

It is very common for programs to open a file in a known location using an untrusted filename. Programs can avoid directory traversal attacks by first validating the filename with a function like [`filepath.IsLocal`](https://pkg.go.dev/path/filepath#IsLocal). Defending against symlink traversal is harder.

I propose adding functions to open a file in a location:

```go
package os

// OpenFileIn opens the named file in the named directory.
//
// If the file contains relative path components (..), no component may
// refer to a location outside the parent directory. The file may not be
// "", an absolute path, or (on Windows) a reserved device name such as "NUL".
// The file may refer to the directory itself (.).
//
// If any component of the named file references a symbolic link
// referencing a location out of the parent directory,
// OpenFileIn returns an error.
//
// OpenFileIn otherwise behaves like OpenFile.
func OpenFileIn(parent, name string, flag int, perm FileMode) (*File, error)

// CreateIn creates or truncates the named file in the named parent directory.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Create].
func CreateIn(parent, name string) (*File, error)

// Open opens the named file in the named parent directory for reading.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Open].
func OpenIn(parent, name string) (*File, error)
```

The `OpenFileIn`, `OpenIn`, and `CreateIn` family of functions safely open a file within a given location, defending against directory traversal, symlinks to unexpected locations, and unexpected access to Windows device files.

----

All modern Unix systems that I know of provide an `openat` call, to open a file relative to an existing directory handle (FD). Windows provides an equivalent (`NtCreateFile` with `ObjectAttributes` including a `RootDirectory`). Of the supported Go ports, I believe only js and plan9 do not support `openat` or an equivalent.

I propose adding support for `openat`-like behavior to `os.File`:

```go
package os

// OpenFile opens the named file in the directory associated with the file f.
//
// If the file contains relative path components (..), no component may
// refer to a location outside the parent directory. The file may not be
// "", an absolute path, or (on Windows) a reserved device name such as "NUL".
//
// If any component of the named file references a symbolic link
// referencing a location out of the parent directory,
// OpenFile returns an error.
func (f *File) OpenFile(name string, flag int, perm FileMode) (*File, error)

// Create creates or truncates the named file in
// the directory associated with the file f.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) Create(name string) (*File, error)

// Open opens the named file in the directory associated with the file f for reading.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) OpenIn(name string) (*File, error)
```

Like the top-level `CreateIn`, `OpenIn`, and `OpenFileIn`, the methods defend against accessing files outside the given directory. This is unlike the default behavior of `openat`, which permits absolute paths, relative paths outside the root, and symlink traversal outside the root. (It corresponds to Linux's `openat2` with the `RESOLVE_BENEATH` flag.)

A property of `openat` is that it follows a file across renames: If you open a directory, rename the directory, and use `openat` on the still-open FD, access is relative to the directory's new location. We cannot support this behavior on platforms which don't have `openat` or an equivalent (plan9 and js). We could fall back to operating purely on filenames, such that `f.OpenIn(x)` is equivalent to `os.OpenIn(f.Name(), x)`. However, this seems potentially hazardous. I propose, therefore, that `File.CreateIn`, `File.OpenIn`, and `File.OpenFileIn` return an `errors.ErrUnsupported` error on these platforms.

----

The above functions defend against symlink traversal that leads outside of the designated root directory. Some users may wish to defend against symlink traversal entirely. Many modern operating systems provide an easy way to disable symlink following: Linux has `RESOLVE_NO_SYMLINKS`, Darwin has `O_NOFOLLOW_ANY`, and some other platforms have equivalents.

I propose adding support for disabling symlink traversal to the `os` package:

```go
const (
	// O_NOFOLLOW_ANY, when included in the flags passed to [OpenFile], [OpenFileIn],
	// or [File.OpenFile], disallows resolution of symbolic links anywhere in the
	// named file.
	//
	// O_NOFOLLOW_ANY affects the handling of symbolic links in all components
	// of the filename. (In contrast, the O_NOFOLLOW flag supported by many
	// platforms only affects resolution of the last path component.) 
	//
	// O_NOFOLLOW_ANY does not disallow symbolic links in the parent directory name
	// parameter of [OpenFileIn].
	//
	// O_NOFOLLOW_ANY does not affect traversal of hard links, Windows junctions,
	// or Plan 9 bind mounts.
	//
	// On platforms which support symbolic links but do not provide a way to
	// disable symbolic link traversal (GOOS=js), open functions return an error
	// if O_NOFOLLOW_ANY is provided.
	O_NOFOLLOW_ANY int = (some value)
)
```

`O_NOFOLLOW_ANY` may be passed to `OpenFile`,  `OpenFIleIn`, or `File.OpenFIle` to disable symlink traversal in any component of the file name. For `OpenFileIn`, symlinks would still be permitted in the directory component.

On platforms which do not support the equivalent of `O_NOFOLLOW_ANY`/`RESOLVE_NO_SYMLINKS` natively, the `os` package will use successive `openat` calls with `O_NOFOLLOW` to emulate it. On platforms with no `openat` (plan9 and js), open operations will return an error when `O_NOFOLLOW_ANY` is specified.
<!-- Generated by Oscar. DO NOT EDIT. {"bot":"gabyhelp","kind":"overview"} -->
@gabyhelp's overview of this issue: https://github.com/golang/go/issues/67002#issuecomment-2593973554
<!-- oscar-end -->