🐛 bug: enforce static root for fs-backed directory serving#4277
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughPathRewrite now conditionally prepends the configured io/fs root prefix to rewritten request paths before normalization and sanitization; a new test verifies the fs root enforces directory boundaries. ChangesStatic FS Root Handling
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4277 +/- ##
=======================================
Coverage 91.21% 91.22%
=======================================
Files 130 130
Lines 12763 12793 +30
=======================================
+ Hits 11642 11670 +28
- Misses 709 710 +1
- Partials 412 413 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request modifies the static middleware to enforce the root directory when a custom filesystem is used and adds a corresponding test case. Feedback highlights a critical path traversal vulnerability where request paths are not properly sanitized before being joined with the root. Additionally, it is recommended to pre-calculate a sanitized root prefix to avoid redundant assignments and to include a specific regression test for directory traversal attempts.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
middleware/static/static.go (1)
210-212: ⚡ Quick winSecurity fix correctly enforces filesystem root.
This logic properly prepends the configured root to the request path when serving from an
io/fs.FS, ensuring files resolve under the intended subdirectory (e.g.,testdata/fs/) rather than the broader FS root. The conditions appropriately guard against empty or current-directory roots.💡 Optional: Add explanatory comment
Consider adding a brief comment to clarify the security purpose:
default: path = path[prefixLen:] + // Enforce the configured root subdirectory within the FS to prevent + // requests from resolving outside the intended static directory. if fileServer.FS != nil && fsRootPath != "" && fsRootPath != "." { path = append([]byte("/"+fsRootPath), path...) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@middleware/static/static.go` around lines 210 - 212, The code in middleware/static/static.go that prepends fsRootPath to the request path when fileServer.FS is non-nil (the if block referencing fileServer.FS, fsRootPath and path) correctly restricts served files to the configured subdirectory; add a short explanatory comment immediately above this if-statement explaining the security intent (prevent path resolution outside the configured FS root by prepending "/"+fsRootPath when fsRootPath is set and not "."), and keep the existing conditional checks unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@middleware/static/static.go`:
- Around line 210-212: The code in middleware/static/static.go that prepends
fsRootPath to the request path when fileServer.FS is non-nil (the if block
referencing fileServer.FS, fsRootPath and path) correctly restricts served files
to the configured subdirectory; add a short explanatory comment immediately
above this if-statement explaining the security intent (prevent path resolution
outside the configured FS root by prepending "/"+fsRootPath when fsRootPath is
set and not "."), and keep the existing conditional checks unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: cc8fa8f6-8c77-42d5-97dd-3d557fd03584
📒 Files selected for processing (2)
middleware/static/static.gomiddleware/static/static_test.go
There was a problem hiding this comment.
Pull request overview
This PR tightens middleware/static behavior for io/fs.FS-backed static serving so that directory roots stay confined to the caller-provided root (preventing unintended access to files outside the intended subdirectory).
Changes:
- Adjust
fasthttp.FSPathRewritelogic forConfig.FSto ensure directory-root requests are resolved under the configuredroot. - Preserve existing behavior for single-file roots and non-
fs.FSconfigurations. - Add a regression test covering the enforced directory-root behavior for
fs.FS.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
middleware/static/static.go |
Updates PathRewrite to prepend the configured root for fs.FS directory-root serving. |
middleware/static/static_test.go |
Adds a regression test to ensure fs.FS directory roots can’t escape the configured subdirectory. |
Comments suppressed due to low confidence (1)
middleware/static/static.go:200
- If isFile(root, fileServer.FS) returns an error, PathRewrite returns the original request path and skips both root enforcement and sanitizePath. With Config.FS set, this can re-expose files from the broader fs.FS (and bypass the extra path validation) when the configured root is invalid/missing or otherwise causes Open/Stat to fail. Consider treating this error as “not a file” and continuing the rewrite+sanitize flow (or returning a guaranteed-missing path) so requests can’t fall back to the wider FS.
checkFile, err := isFile(root, fileServer.FS)
if err != nil {
return path
}
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/108ccab2-b758-45f5-973b-ea4a263ec44b Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/108ccab2-b758-45f5-973b-ea4a263ec44b Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/108ccab2-b758-45f5-973b-ea4a263ec44b Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/108ccab2-b758-45f5-973b-ea4a263ec44b Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Applied the requested follow-up changes in 8dbb346: the fs root prefix is now precomputed outside the hot path, the rewrite path assembly stays allocation-light, and the regression test now asserts the response body comes from the confined fs root content. |
ReneWerner87
left a comment
There was a problem hiding this comment.
Nice direction, but I do not think this is ready to merge. The intended root boundary is still bypassable, and the regression test only covers the trivial double-prefix case rather than actual traversal attempts. Inline comments below.
Summary of concerns
-
Path traversal bypass.
fsRootPrefixis prepended beforesanitizePath, andsanitizePathcallspathpkg.Cleanwhich resolves..segments before the..check (static.go:75) ever runs. A request like/static/%2E%2E/index.html(or any non-normalised..input) becomes/fs/../index.htmlafter the prefix prepend, and Clean rewrites that to/index.html, escaping the configuredfs/subdirectory. The..check at line 75 is effectively dead code for absolute paths becausepathpkg.Cleanalways resolves them away first. -
isFileerror bypass. IfisFile(root, fileServer.FS)returns an error (e.g. the configured root is missing or unreadable in thefs.FS),PathRewritereturns the rawfctx.Path()and skips both the newfsRootPrefixprepend andsanitizePath. WithConfig.FSset, that re-exposes the broaderfs.FSand bypasses the very defence this PR is adding. Already flagged by Copilot, still unaddressed. -
Test coverage is too narrow for a security fix.
Test_Static_FS_RootDirectoryEnforcedonly checks that/static/fs/index.html404s. None of the realistic attack shapes are covered:.., URL-encoded%2E%2E, mixed..%2F, backslash variants, empty path, trailing slash. A regression test that does not exercise the attack class it claims to prevent is not a regression test for that class. -
No comment on the security-critical block. In six months no one will remember why
fsRootPrefixexists. CodeRabbit's nitpick is fair.
Suggested fixes
- Move the prefix prepend after
sanitizePath, or perform the..rejection on the unescaped-but-not-yet-cleaned input. - For the
isFileerror path, either treat the error ascheckFile=falseand continue through the normal rewrite + sanitize flow, or return the/__fiber_invalid__sentinel so the FS layer 404s. Do not return the raw path. - Extend the test to cover
..,%2E%2E,..%2F, backslashes,/static, and/static/. - Add a short comment explaining why
fsRootPrefixis needed (FS root is forced to""to get clean paths from fasthttp, so we have to re-prepend the caller-specified subdir inPathRewrite).
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/0b3e54f1-0350-4b89-acd5-a1438722023a Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/0b3e54f1-0350-4b89-acd5-a1438722023a Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/0b3e54f1-0350-4b89-acd5-a1438722023a Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Addressed in ca6f2a1: the fs-backed rewrite now sanitizes the stripped request path before re-anchoring it under the configured root, rejects traversal attempts, stops falling back to the broader fs.FS when root validation fails, and adds regression coverage for traversal and missing-root cases.
Fixed on ca6f2a1. I reproduced the failure with |
|
@copilot The current implementation is vulnerable to a critical path traversal vulnerability. By prepending the root directory to the raw request path before sanitization, an attacker can use .. segments to escape the intended root. For example, a request to /static/../secret.txt would result in /root/../secret.txt, which sanitizePath (via pathpkg.Clean) resolves to /secret.txt, disclosing files outside the static subdirectory. Additionally, the current logic may fail to join paths correctly if the stripped path doesn't start with a slash (e.g., resulting in /rootfile.txt instead of /root/file.txt). To fix this, the request path must be cleaned to resolve any .. segments relative to the request root before joining it with the configured static root. if rootPrefix != "" {
// Clean the request path to prevent traversal out of the root and ensure proper joining
relPath := pathpkg.Clean("/" + utils.UnsafeString(path))
path = utils.UnsafeBytes(pathpkg.Join(rootPrefix, relPath))
} |
Checked against the current branch state in ca6f2a1: the request suffix is sanitized before the trusted fs root is re-prepended, so the traversal shape in this comment no longer reproduces. I re-ran |
|
@copilot address the review comment raised here https://github.com/gofiber/fiber/pull/4277/changes#r3216778895 Also, please include a comprehensive suite of tests to cover various directory traversal scenarios. |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/88a3e9aa-52fb-4ddd-b32e-5c269cf8d5a5 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
|
@claude[agent] Please conduct a security audit of this implementation and address any identified issues. |
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/88a3e9aa-52fb-4ddd-b32e-5c269cf8d5a5 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/88a3e9aa-52fb-4ddd-b32e-5c269cf8d5a5 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/88a3e9aa-52fb-4ddd-b32e-5c269cf8d5a5 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/88a3e9aa-52fb-4ddd-b32e-5c269cf8d5a5 Co-authored-by: gaby <835733+gaby@users.noreply.github.com>
Addressed in 77ac263. The static fs regression test now includes a broader traversal matrix covering raw |
Completed comprehensive security audit of the static middleware implementation (commit 77ac263). Audit Summary: ✅ SECURE FindingsNo critical, high, or medium severity vulnerabilities identified. The implementation correctly prevents:
Defense Layers
Test Coverage
Compliance
Validationmake audit # ✓ go mod verify, vet, govulncheck pass
make format # ✓ no changes
make lint # ✓ 0 issues
make test # ✓ 3308 tests passRecommendation: Implementation is production-ready from a security perspective. |
Motivation
Config.FS-backed static handlers to set the fasthttp FS root to"."and rely onPathRewriteto map requests, which allowed directory-root requests to resolve from the broaderfs.FSinstead of the caller-specifiedrootand could disclose files outside the intended static subdirectory.io/fs.FSremain confined to the providedrootwhile preserving existing behavior for single-file roots and non-FS usage.Description
fileServer.PathRewriteinmiddleware/static/static.goto prepend the configuredroot(when non-empty and not".") for directory-root cases whenConfig.FSis set, ensuring requests are resolved under the intended static root.checkFile(single-file root) branches and behavior for non-FS roots so file-root semantics remain unchanged.Test_Static_FS_RootDirectoryEnforcedinmiddleware/static/static_test.goto assert thatNew("fs", Config{FS: os.DirFS("../../.github/testdata")})serves/static/index.htmlbut rejects/static/fs/index.html.