By Max Sours, Senior Software Engineer
May 29, 2025
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.
The Go Gopher mascot was created by Renee French and is licensed under the Creative Commons 4.0 Attribution License.
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.
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.
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.
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.
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.
Thanks for reading. Here are the main takeaways:
If you have any questions or concerns, please feel free to reach out to Contrast’s support team.
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.
Get the latest content from Contrast directly to your mailbox. By subscribing, you will stay up to date with all the latest and greatest from Contrast.