Back to blog

Go Agent Dev Diary: Navigating os.Root and path-traversal vulnerabilities

The latest Go release — Go 1.24, released in February 2025 — introduced a significant security enhancement: the os.Root type. 

os.Root  is part of the Golang standard library as of Go 1.24 and is hence turned on automatically for all Go users. Designed to provide a crucial layer of defense against path-traversal attacks, os.Root restricts file 0 3.system operations to a specified root directory. However, as with any security measure, its effectiveness isn't absolute. There are specific scenarios where os.Root can still be vulnerable, and understanding these nuances is essential for robust Application Security. 

This post aims to delve into these potential vulnerabilities and explain how Contrast Security addresses them.

Go gopher

The Go Gopher mascot was created by Renee French and is licensed under the Creative Commons 4.0 Attribution License.

 

os.Root and attack surface

Specifically, os.Root operates by blocking file system operations that attempt to access files or directories outside a designated root. While this mechanism significantly reduces the attack surface, determining whether os.Root is susceptible to an attack often hinges on knowing which files and folders within the root are safe for untrusted access. 

A security tool like Contrast can not and should not interrogate customers to find out which files they would like to keep private. Therefore, we must make informed assumptions to ensure security while minimizing false positives. For our Contrast Assess product, we operate under the assumption that developers utilizing os.Root are aware of path-traversal risks and have configured their file systems accordingly. Conversely, for Contrast Application Detection and Response (ADR), we adopt a more conservative stance, treating all uses of os.Root as potentially vulnerable and acting proactively to mitigate potential threats. This represents a refinement of our earlier approach, which treated all os.Root implementations as inherently risky, a strategy that resulted in excessive noise for our users.

Let’s get right to the point before we do a deep dive into why we made this decision. In Go Agent versions 7.3.0 and later, we treat os.Root methods as vulnerable to path-traversal for ADR in all cases, and for Assess when we detect that os.Root has been given an unsafe root. (Initializing an os.Root with untrusted data is always a vulnerability and will be treated as such.)

// Assess does not register vulnerability, still guarded by ADR
os.OpenInRoot("/path/to/public/facing/dir", userInput)

// Assess detects vulnerability, guarded by ADR
os.OpenInRoot("/", userInput)

 

This is a relaxation from our v7.2.0 implementation which treated os.Root methods as vulnerable to path-traversal in all cases for both Assess and ADR. The Go Agent did not support new go1.24 features before v7.2.0.

Background

In go1.24, Go added the new type os.Root that confines file system operations to a specified base directory. os.Root is initialized by passing in a desired base directory into os.OpenRoot. Then os.Root methods correspond to various file system operations.

r, err :=
os.OpenRoot("./base/dir")

if err != nil {
    log.Fatal(err)
}
// will open the file located at ./base/dir/file
f, err := r.Open("file")
if err != nil {
    log.Fatal(err)
}
// os.OpenInRoot is equivalent to the above code
f, err = os.OpenInRoot("./base/dir", "file")
if err != nil {
    log.Fatal(err)
}

 

This blog post written by the Go team explains in greater detail why they made os.Root and what type of attacks it is designed to prevent. While these new APIs add a layer of defense, it’s still possible to misuse them, leaving the application vulnerable.

When is os.Root vulnerable?

Let’s start with an easy one: initializing an os.Root with user-controlled data is always unsafe and leaves the application vulnerable to path traversals. Even if the act of initializing an os.Root with untrusted data was not vulnerable (which it is), it is easy to see how bad it could be if an attacker had all os.Root methods acting on say, /etc/.

A trickier case is when the user does not control the root, but the developer picks an unsafe directory. For example, if the application specifies "/" as a root, os.Root’s protections do very little because the root will still allow access to the entire filesystem. 

There are more subtle issues as well. A developer might have a directory structure like this:

userfiles
├── user1
│   └── user1.txt
└── user2
    └── user2.txt

 

If the developer sets the root to userfiles, this will still be vulnerable to path traversal.

root, err := os.OpenRoot("./userfiles")
if err != nil {
    log.Fatal(err)
}

// request logged in as user1
// assume validation of some sort checks to make sure the path starts with user1
file, err := root.Open("user1/../user2/user2_private_info.txt")
if err != nil {
 
log.Fatal(err)
}

data, err := io.ReadAll(file)
if err != nil {
  log.Fatal(err)
}
log.Println(string(data))

 

Something like this is much harder for Contrast to account for, since identifying the vulnerability requires knowledge of which files and folders are considered safe for an untrusted user to access.

In other words, os.Root prevents traversal outside of the specified root, but does nothing to prevent access to unintended files within a root.

Because of this, our original support for os.Root in v7.2.0 assumed that all instances of os.Root could be vulnerable, but we were worried that this would generate a lot of false positives. After all, if a developer is using os.Root’s APIs, we expect they’re aware of how path-traversal vulnerabilities work and are trying to do the right thing; this means os.Root is probably being used safely most of the time. After much discussion internally, we decided to relax our assumptions a bit.

Detecting vulnerabilities in os.Root methods

We always aim to minimize noise by avoiding false positives. This means that we don’t want to report that something is vulnerable to path-traversal if the root only contains data that is safe for a user to access. Unfortunately, this isn’t really something we can know.

For now, we’ve decided that if you’re using os.Root, you’re probably aware of how to mitigate against path-traversal and aren’t initializing the root in a location that’s too permissive, but we’ll still keep an eye on things if the root directory is something like / or /etc.

Protecting against attacks in os.Root methods

When protecting against attacks, the calculus is different. If we have flagged an input in Protect mode, it is very likely an attack attempt. It is much more acceptable for us to block a suspicious input even if we do not necessarily need to.

payload := "../../../../../../../etc/passwd"

os.OpenInRoot("./safe/dir", payload)

 

Note that Contrast will always block before the function can execute. (Otherwise the vulnerability would have already been exploited.) This means that if this code runs with the agent in Protect mode, all that will happen is the Go Agent will block the attack before os.Root can return an error. If ADR’s monitor mode is enabled, the agent will still say the vulnerability was exploited even if os.Root stopped the path-traversal. This does not mean that os.Root is letting attacks through. All it means is that Contrast saw the attack enter the os.Root method code, and did not stop it. (Which is the correct behavior if Contrast is in Monitor mode.) We recognize this could cause more noise than normal, but it is important that we err on the side of caution when blocking likely attacks in a production environment.

The bottom line is this: Just because we have reported an attack on an os.Root method does not mean that the attack has traversed past the os.Root base directory. That should not happen. It only means that the developer needs to verify that their os.Root is pointing somewhere safe.

In sum, below are the key benefits of the new os.Root type, as well as the instances where it may still be susceptible to attack: Understanding both aspects is crucial for effective security implementation.

Key benefits of Go 1.24's os.Root

  • Enhanced security: Provides a new layer of defense against path-traversal attacks.
  • Restricted file system access: Blocks file system operations that attempt to access files or directories outside of a specified root directory.
  • Reduced attack surface: By confining file system access, os.Root significantly reduces the application's potential attack surface.
  • Part of standard library: Integrated into the Golang standard library as of Go 1.24, making it readily available for all Go users.
  • Automatic enablement: Turned on automatically for all Go users in Go 1.24.

Instances where os.Root can still be vulnerable

  • Initializing os.Root with user-controlled data.
  • Setting an overly broad or permissive root directory, such as "/".
  • Path traversal within the specified root, allowing access to unintended files within that root.

Conclusion

Thanks for reading. Here are the main takeaways: 

  • Go 1.24's os.Root improves security against path-traversal attacks but isn't perfect. Contrast Security helps with intelligent detection and protection.
  • Don't use untrusted data to initialize os.Root, and avoid broad root directories.
  • Understand that os.Root doesn't stop intra-root traversal. 
  • Contrast Security's Go Agent v7.3.0 has updated detection and protection methods. 
  • Go Agent v7.3.0 has been released and contains the new changes mentioned here. Here are the release notes

If you have any questions or concerns, please feel free to reach out to Contrast’s support team.

Call us

Max Sours, Senior Software Engineer

Max Sours, Senior Software Engineer

Max Sours is a Senior Software Engineer on the Go Agent Team at Contrast Security. Max started his career at Contrast, where he gained a keen interest in Golang and Application Security. Max enjoys tackling the exciting and unique problems that working on a Contrast Agent team presents.

Enlarged Image