Skip to content

🐛 bug: enforce static root for fs-backed directory serving#4277

Merged
ReneWerner87 merged 16 commits into
mainfrom
propose-fix-for-static-fs.fs-vulnerability
May 15, 2026
Merged

🐛 bug: enforce static root for fs-backed directory serving#4277
ReneWerner87 merged 16 commits into
mainfrom
propose-fix-for-static-fs.fs-vulnerability

Conversation

@gaby

@gaby gaby commented May 11, 2026

Copy link
Copy Markdown
Member

Motivation

  • A prior change caused Config.FS-backed static handlers to set the fasthttp FS root to "." and rely on PathRewrite to map requests, which allowed directory-root requests to resolve from the broader fs.FS instead of the caller-specified root and could disclose files outside the intended static subdirectory.
  • The goal is to ensure directory roots configured with io/fs.FS remain confined to the provided root while preserving existing behavior for single-file roots and non-FS usage.

Description

  • Update fileServer.PathRewrite in middleware/static/static.go to prepend the configured root (when non-empty and not ".") for directory-root cases when Config.FS is set, ensuring requests are resolved under the intended static root.
  • Preserve existing checkFile (single-file root) branches and behavior for non-FS roots so file-root semantics remain unchanged.
  • Add Test_Static_FS_RootDirectoryEnforced in middleware/static/static_test.go to assert that New("fs", Config{FS: os.DirFS("../../.github/testdata")}) serves /static/index.html but rejects /static/fs/index.html.

@gaby gaby added the aardvark label May 11, 2026
Copilot AI review requested due to automatic review settings May 11, 2026 03:35
@gaby gaby requested a review from a team as a code owner May 11, 2026 03:35
@gaby gaby requested review from ReneWerner87, efectn and sixcolors May 11, 2026 03:35
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

PathRewrite 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.

Changes

Static FS Root Handling

Layer / File(s) Summary
Compute fsRootPrefix in New
middleware/static/static.go
Compute a leading-slash byte prefix from root when config.FS is set and root is non-empty, not ".", and not a regular file.
PathRewrite prefixing and normalization
middleware/static/static.go
After removing the route prefix, PathRewrite prepends fsRootPrefix when present, applies trailing-slash normalization, ensures the path starts with /, and sanitizes it before lookup.
Root enforcement test
middleware/static/static_test.go
Adds Test_Static_FS_RootDirectoryEnforced to assert /static/index.html is served (200) and /static/fs/index.html returns 404, confirming the fs root boundary is enforced.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • gofiber/fiber#3600: Modifies PathRewrite and path-handling in static middleware; related to fs/path handling changes.
  • gofiber/fiber#3006: Introduced io/fs-based roots support and related path resolution changes.
  • gofiber/fiber#4064: Hardens sanitizePath and filesystem-specific validations in static middleware.

Suggested reviewers

  • sixcolors
  • ReneWerner87
  • efectn

Poem

🐰 I hopped through routes with a curious stare,
Found a root lost in io/fs air.
I stitched a prefix, neat and bright,
Now files rest safe inside their site.
Hooray — no more wandering paths tonight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: enforcing static root boundaries for fs-backed directory serving to fix a security vulnerability.
Description check ✅ Passed The description clearly explains the motivation, the specific changes made, and includes a test case. However, it does not fill in the structured template sections like 'Changes introduced', 'Type of change', or 'Checklist'.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch propose-fix-for-static-fs.fs-vulnerability

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.22%. Comparing base (33b7fd4) to head (f0476b3).

Files with missing lines Patch % Lines
middleware/static/static.go 88.88% 3 Missing and 2 partials ⚠️
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     
Flag Coverage Δ
unittests 91.22% <88.88%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread middleware/static/static.go Outdated
Comment thread middleware/static/static.go Outdated
Comment thread middleware/static/static_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
middleware/static/static.go (1)

210-212: ⚡ Quick win

Security 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

📥 Commits

Reviewing files that changed from the base of the PR and between c48bbc2 and c53660c.

📒 Files selected for processing (2)
  • middleware/static/static.go
  • middleware/static/static_test.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.FS PathRewrite logic for Config.FS to ensure directory-root requests are resolved under the configured root.
  • Preserve existing behavior for single-file roots and non-fs.FS configurations.
  • 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
					}

Comment thread middleware/static/static.go Outdated
Comment thread middleware/static/static_test.go Outdated
@gaby

gaby commented May 11, 2026

Copy link
Copy Markdown
Member Author

@copilot apply changes based on the comments in this thread

Copilot AI and others added 4 commits May 11, 2026 03:53
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>

Copilot AI commented May 11, 2026

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

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 ReneWerner87 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Path traversal bypass. fsRootPrefix is prepended before sanitizePath, and sanitizePath calls pathpkg.Clean which 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.html after the prefix prepend, and Clean rewrites that to /index.html, escaping the configured fs/ subdirectory. The .. check at line 75 is effectively dead code for absolute paths because pathpkg.Clean always resolves them away first.

  2. isFile error bypass. If isFile(root, fileServer.FS) returns an error (e.g. the configured root is missing or unreadable in the fs.FS), PathRewrite returns the raw fctx.Path() and skips both the new fsRootPrefix prepend and sanitizePath. With Config.FS set, that re-exposes the broader fs.FS and bypasses the very defence this PR is adding. Already flagged by Copilot, still unaddressed.

  3. Test coverage is too narrow for a security fix. Test_Static_FS_RootDirectoryEnforced only checks that /static/fs/index.html 404s. 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.

  4. No comment on the security-critical block. In six months no one will remember why fsRootPrefix exists. 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 isFile error path, either treat the error as checkFile=false and 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 fsRootPrefix is needed (FS root is forced to "" to get clean paths from fasthttp, so we have to re-prepend the caller-specified subdir in PathRewrite).

Comment thread middleware/static/static.go
Comment thread middleware/static/static.go Outdated
Comment thread middleware/static/static_test.go
Copilot AI and others added 3 commits May 12, 2026 12:42

Copilot AI commented May 12, 2026

Copy link
Copy Markdown
Contributor

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 travers...

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.

@copilot fix the failing unit tests, use "-race".

Fixed on ca6f2a1. I reproduced the failure with go test -race, corrected the broken regression test, and re-ran the static middleware tests with -race plus the repo validation commands.

Copilot finished work on behalf of gaby May 12, 2026 12:54
@gaby

gaby commented May 13, 2026

Copy link
Copy Markdown
Member Author

@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))
}

Copilot AI commented May 13, 2026

Copy link
Copy Markdown
Contributor

@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 ...

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 go test -race ./middleware/static -run 'Test_Static_FS_RootDirectoryEnforced|Test_Static_FS_MissingRootDoesNotFallback' -count=1 and also probed /static/../index.html, /static/%2E%2E/index.html, /static/..%2Findex.html, and /static/..%5Cindex.html; they all return 404 on ca6f2a1.

Copilot finished work on behalf of gaby May 13, 2026 03:05
@gaby

gaby commented May 13, 2026

Copy link
Copy Markdown
Member Author

@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.

@gaby

gaby commented May 13, 2026

Copy link
Copy Markdown
Member Author

@claude[agent] Please conduct a security audit of this implementation and address any identified issues.

Copilot AI commented May 13, 2026

Copy link
Copy Markdown
Contributor

@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...

Addressed in 77ac263. The static fs regression test now includes a broader traversal matrix covering raw .., encoded/double-encoded parent segments, encoded slash and backslash variants, nested traversal attempts, mixed separators, null bytes, plus the allowed /static, /static/, and nested-file success paths with body and MIME-type assertions.

@Claude

Claude AI commented May 13, 2026

Copy link
Copy Markdown
Contributor

@claude[agent] Please conduct a security audit of this implementation and address any identified issues.

Completed comprehensive security audit of the static middleware implementation (commit 77ac263).

Audit Summary: ✅ SECURE

Findings

No critical, high, or medium severity vulnerabilities identified. The implementation correctly prevents:

  • Path traversal attacks (raw .., encoded %2E%2E, double-encoded)
  • Encoding bypass techniques (mixed encoding, alternative separators)
  • Directory escape attempts (backslashes, null bytes, UNC paths)
  • Root boundary violations (missing root fallback, double-prefix)

Defense Layers

  1. Pre-sanitization check (static.go:248-252): hasParentDirSegment catches traversal attempts before path manipulation
  2. Input validation (static.go:264): sanitizePath with recursive unescaping, normalization, and post-clean ".." rejection
  3. Secure anchoring (static.go:269-274): Root prefix prepended after sanitization (correct order)

Test Coverage

  • All 22 regression test cases pass with race detection
  • Attack scenarios: 18 traversal variants blocked, 4 valid paths allowed
  • Edge cases: missing roots, file vs directory roots, empty paths

Compliance

  • ✅ OWASP A01:2021 (Broken Access Control)
  • ✅ CWE-22, CWE-23, CWE-36, CWE-73 (Path Traversal variants)

Validation

make audit    # ✓ go mod verify, vet, govulncheck pass
make format   # ✓ no changes
make lint     # ✓ 0 issues
make test     # ✓ 3308 tests pass

Recommendation: Implementation is production-ready from a security perspective.

Claude finished work on behalf of gaby May 13, 2026 03:30
@ReneWerner87 ReneWerner87 merged commit 24a6d1a into main May 15, 2026
20 of 21 checks passed
@ReneWerner87 ReneWerner87 deleted the propose-fix-for-static-fs.fs-vulnerability branch May 15, 2026 10:19
@github-project-automation github-project-automation Bot moved this to Done in v3 May 15, 2026
@ReneWerner87 ReneWerner87 modified the milestones: v3, v3.3.0 May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants