Skip to content
mediumCVSS 5.3CWE-22A01:2021 – Broken Access Control

Go

No dynamic include exists, so there is no code-execution variant at all. What remains is traversal, and the standard library's file server already rejects it.

Why Go is the safest column

Go has no include, no require, no dynamic import, and no template loader that will compile arbitrary source at request time. Code is linked at build time. There is no mechanism by which a request parameter can cause new code to run.

That removes the entire Escalation to RCE section and every remote-inclusion variant. A Go file-inclusion finding is a read primitive, full stop.

The standard library is also unusually careful: http.ServeFile explicitly rejects any request path containing .., and http.Dir does the same when opening. The bugs come from code that goes around them.

The mistakes

Gothe sinksVulnerable
// 1. Reading a path built from a request parameter. filepath.Join CLEANS
//    the result — it removes .. — but cleaning is not confining. A cleaned
//    path that escaped is still escaped.
func handler(w http.ResponseWriter, r *http.Request) {
    p := filepath.Join("data", r.URL.Query().Get("file"))
    // file=../../etc/passwd  ->  Join cleans to "../etc/passwd"
    // ... which is outside "data". Cleaned, and still wrong.
    b, _ := os.ReadFile(p)
    w.Write(b)
}

// 2. ServeFile with a path from the request. The docs warn about this
//    directly: ServeFile rejects .. in r.URL.Path, but NOT in a name you
//    construct yourself and pass as the third argument.
http.ServeFile(w, r, "data/"+r.URL.Query().Get("file"))

// 3. Prefix check on the un-cleaned string.
p := "data/" + name
if strings.HasPrefix(p, "data/") {     // always true — it was just prepended
    os.ReadFile(p)
}

// 4. Template parsing from a request-derived name. Not code execution —
//    Go templates are not a general language — but it does let the caller
//    choose which template renders, and ParseFiles reads arbitrary files.
tmpl := template.Must(template.ParseFiles(r.URL.Query().Get("tpl")))

The fix

GoSecure
// Go 1.24 added os.Root, which confines every operation to a directory at
// the syscall level — it uses openat2 with RESOLVE_BENEATH on Linux. This
// is the correct answer on a modern Go and it also closes the symlink case
// that a string comparison cannot see.

root, err := os.OpenRoot("/app/data")
if err != nil { log.Fatal(err) }
defer root.Close()

func handler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("file")

    f, err := root.Open(name)          // escapes are refused by the kernel
    if err != nil {
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    defer f.Close()
    io.Copy(w, f)
}

embed.FS removes the problem

If the files are known at build time — templates, static assets, migrations — embed them:

import "embed"

//go:embed templates/*
var templates embed.FS

An embed.FS has no .. semantics and cannot reach the real filesystem at all. A traversal payload against it simply names a file that does not exist. This removes the entire bug class for embedded content, and it is the single most effective thing a Go service can do about it.

The same applies to io/fs more generally: fs.Sub(fsys, "templates") gives you a filesystem rooted at a subdirectory, and fs.ValidPath rejects .. and absolute paths by definition of the interface. Code written against fs.FS rather than os is confined by construction.

What to grep for

Bash
# Read sinks
grep -rnE 'os\.(ReadFile|Open|OpenFile)|ioutil\.ReadFile' --include='*.go' . \
  | grep -E 'r\.URL|Query\(|FormValue|mux\.Vars|c\.Param'

# ServeFile with a constructed name — the documented footgun
grep -rn 'http.ServeFile' --include='*.go' .

# Path building from request data
grep -rnE 'filepath\.Join|path\.Join' --include='*.go' . \
  | grep -E 'r\.URL|Query\(|FormValue|Param'

# Template parsing with a dynamic name
grep -rn 'ParseFiles\|ParseGlob' --include='*.go' .

# Archive extraction
grep -rn 'zip.OpenReader\|tar.NewReader' --include='*.go' .

# The correct calls, to confirm they are present
grep -rn 'os.OpenRoot\|EvalSymlinks\|http.Dir\|embed.FS\|fs.Sub' --include='*.go' .