From c1edf7f45836b3e8c1bb9a75ce62fe0ed4f82371 Mon Sep 17 00:00:00 2001 From: Brendan Creane Date: Fri, 17 Jul 2026 10:17:06 -0700 Subject: [PATCH] wgengine/router/osrouter: sanitize interfaceV6UsableForTun path with os.OpenInRoot (#20505) interfaceV6UsableForTun interpolates the interface name into a /proc path. A plain filepath.Join + os.Open only cleans the path, so a tunname with ".." (or a symlinked component) could read outside /proc/sys/net/ipv6/conf. Open under that fixed directory with os.OpenInRoot, which rejects any path escaping the root (openat-based, so also TOCTOU-resistant), still using filepath.Join to build the relative name. See https://go.dev/blog/osroot. Updates #20447 Signed-off-by: Brendan Creane --- wgengine/router/osrouter/router_linux.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/wgengine/router/osrouter/router_linux.go b/wgengine/router/osrouter/router_linux.go index 372e29282..3e3998850 100644 --- a/wgengine/router/osrouter/router_linux.go +++ b/wgengine/router/osrouter/router_linux.go @@ -9,6 +9,7 @@ import ( "bytes" "errors" "fmt" + "io" "net" "net/netip" "os" @@ -963,14 +964,21 @@ func interfaceV6UsableForTun(tunname string) bool { if tunname == "" { return false } - bs, err := os.ReadFile(filepath.Join("/proc/sys/net/ipv6/conf", tunname, "disable_ipv6")) + // Open under conf/ with os.OpenInRoot so a "../" or symlink in tunname can't + // escape the directory. + f, err := os.OpenInRoot("/proc/sys/net/ipv6/conf", filepath.Join(tunname, "disable_ipv6")) if err != nil { - // A missing directory/knob means IPv6 isn't up on the interface, so - // it's unavailable. Any other error (e.g. EACCES) means the knob - // exists but we couldn't read it; assume IPv6 is usable rather than - // skipping it on a transient error. + // A missing directory/knob means IPv6 isn't up on the interface, so it's + // unavailable. Any other error (e.g. EACCES, or tunname escaping the + // root) means we couldn't read the knob; assume IPv6 is usable rather + // than skipping it on a transient or defensive error. return !os.IsNotExist(err) } + defer f.Close() + bs, err := io.ReadAll(f) + if err != nil { + return true // couldn't read; assume usable + } disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs))) if err != nil { return true // unparseable; assume usable