AL+

Language: RO (română) EN (English)

Email us: paul@alplustech.com

Looking back at the week of 12–18 March 2025

Validate the file operation, not a name you will resolve again

Go's os.Root moves traversal resistance into directory-relative operations. Review symlink races, platform limits and later fixes before choosing a version.

Citiți articolul în română

A filename that passed validation is not proof that a later operation will open the intended file. If another actor can change the filesystem between the check and the open, the same name can resolve somewhere else. Untrusted file access should therefore use an operation designed to resist traversal relative to a trusted directory, while retaining application authorization and resource limits around it.

Looking back from November 2026, the Go team's 12 March 2025 article on traversal-resistant file APIs explained this boundary through os.Root, introduced in Go 1.24. The design makes the directory-relative operation the protection point. It does not make the whole process a sandbox, and the original announcement is insufficient evidence for choosing a secure version today.

A checked string can outlive the fact it described

Consider an illustrative document importer that accepts a relative name. Removing .. components might prevent a simple textual escape. It says nothing about a symbolic link already inside the import directory. Resolving that link and checking the resulting location looks stronger, but a later ordinary open resolves the path again.

That second resolution creates a gap. Another actor might replace a directory component with a link after validation. The application then uses a name whose spelling has not changed but whose destination has. The vulnerability is a relationship between two operations and mutable filesystem state, rather than simply an unusual input string.

This is why adding more string validation does not necessarily solve the race. A check records something true at a particular moment. Unless the subsequent operation preserves the relevant relationship, the program relies on the filesystem remaining unchanged. That may be a valid assumption for a tightly controlled deployment, but it should be part of the threat model rather than an accidental dependency.

A business-facing consequence is misplaced trust in an import feature. Allowing users to supply filenames is also allowing them to influence which records the service reads or replaces. If extraction can create links, even an archive can introduce filesystem state that later entries encounter. The application needs a rule about the actual operation, not only about the spelling accepted by a form.

A directory handle changes the relationship

The March article describes opening a root directory and performing relative operations through it. Its intended contract rejects escapes through parent components or symlinks while allowing links that stay inside the root. The distinction matters: traversal resistance need not mean banning every link or every path containing a parent component.

For an application design, the root becomes an explicit dependency of file-handling code. Instead of handing a helper an unrestricted base-path string and asking it to join names, pass the capability through the component that performs the operation. That makes it easier to review whether a later step accidentally returns to unrestricted path-based access.

The handle does not decide which customer owns a document. An operation can remain within a directory and still read another customer's file if the directory contains both. Authorization needs its own boundary. A root chosen too broadly cannot repair that decision. Choose the directory according to the application's ownership model, and check the user's permission for the intended operation.

Nor does traversal resistance bound the amount of work. A valid file may be too large, an accepted archive may contain too many entries, or an allowed read may disclose data the caller should not see. These are illustrative limits to design separately, not defects that the directory API claims to solve.

Platform qualifications belong in the design

The historical article explicitly distinguishes implementations. On Unix, it describes directory-descriptor operations and excludes mount-point containment, including bind mounts. On Windows, an open root handle affects renaming and deletion. For WASI, resistance depends on the implementation. For GOOS=js, the Node filesystem interface leaves symlink-validation races; Plan 9 uses lexical sanitization without symlinks.

These qualifications prevent a universal sandbox claim. A service that executes untrusted programs needs a much broader boundary than a directory-relative file API. The process can still have other file handles, network access or unrelated privileges. Restricting one set of file operations does not revoke those capabilities.

Later fixes change present-day advice

The Go vulnerability database records CVE-2025-22873, a parent-directory escape with a limited effect: opening the parent directory itself, not its contents or further ancestors. Its record lists the Go 1.24 branch fix as 1.24.3. The database record was published on 4 February 2026; that record date should not be confused with the original feature announcement.

A later record, CVE-2026-39822, published on 7 July 2026, describes Unix symlink escape with a trailing slash and lists fixes in 1.25.12 and 1.26.5, plus 1.27.0-rc.2. Those are fix milestones for that issue, not a claim that these are today's preferred releases. A real deployment should check its maintained toolchain against current advisories rather than stop at the introduction of os.Root.

Keep the response proportionate

The strongest counterargument is that an application may accept only fixed internal filenames in a filesystem that untrusted actors cannot modify. In that narrower setting, ordinary path handling with appropriate input constraints may be sufficient. Adding a new abstraction without a relevant threat can complicate maintenance.

Where untrusted names or mutable directory contents are involved, review one complete operation: choose the root, authorize the user, open through the protected interface and keep subsequent work within the intended boundary. For custom software development, ask for that chain to be explained alongside the deployed platform and patch state. A clean filename is useful input; a protected operation is the stronger contract.