Ferron Windows Path Handling: A Routing Bypass
Ferron’s request handling contains a mismatch between how it matches URL locations and how Windows interprets file paths. With the backslash rejection setting disabled, a request can miss a location rule and still resolve to a file beneath the configured web root.
The behavior requires a non-default configuration: Ferron’s default setting rejects backslashes in URL paths. The issue affects deployments that explicitly turn off that protection.
The mismatch is easiest to see as two interpretations of the same decoded request:
flowchart TD
R["Request: /private%5Csecret.txt"] --> D["Decoded path: /private\secret.txt"]
D --> L["Ferron location matcher<br/>Splits only on /<br/>One segment; /private rule skipped"]
D --> W["Windows file resolver<br/>Treats \ as a separator<br/>Resolves private/secret.txt"]
L --> X["No 403 response"]
W --> X
X --> Y["Protected file is served"]
The setting
Ferron has a global url_reject_backslash option. It defaults to true. With that default, literal and percent-encoded backslashes in URL paths are rejected; the request used in this analysis receives 400 Bad Request.
The behavior discussed here requires explicitly setting it to false:
1
2
3
4
5
{
http {
url_reject_backslash false
}
}
A backslash does not have the same meaning everywhere. It may be treated as an ordinary character by URL routing code and as a directory separator by Windows filesystem code. Disabling the rejection allows the request to reach those later stages, where differing interpretations become relevant.
The routing rule
Consider a site that serves files from a web root and denies requests to a private directory:
1
2
3
4
5
6
7
*:8080 {
root "C:/ferron-test/www"
location /private {
status 403
}
}
A normal request for /private/secret.txt should match the /private location and return 403 Forbidden.
The request of interest is:
1
/private%5Csecret.txt
%5C is the percent-encoded form of a backslash. Ferron’s URL canonicalizer decodes it for routing, yielding /private\\secret.txt. The location resolver splits paths on forward slashes, however, so it sees private\\secret.txt as one segment rather than a private segment followed by secret.txt. That prevents the /private rule from matching.
Static-file resolution follows a different path. It percent-decodes the request URI and uses Rust’s Path::components() to extract filesystem components. On Windows, backslashes are path separators. The same decoded string therefore resolves as the private directory and then secret.txt.
| Stage | Expected interpretation |
|---|---|
| Location matching | One segment: private\\secret.txt |
| Windows file resolution | Directory private, then file secret.txt |
Relevant code paths
The relevant Ferron code follows this sequence:
modules/http-server/src/handler/mod.rs:575–581reads the backslash-rejection setting, defaulting totrue, whilerequest_utils.rs:116–119accepts the path when rejection is disabled.modules/http-server/src/util/canonicalize_url.rs:346–363decodes%5Cinto a backslash for routing.modules/http-server/src/config/resolver/resolver.rs:280–296andtree.rs:377–385split and compare location segments using/.modules/http-server/src/handler/file_pipeline.rs:102–117, 691–715percent-decodes the request path and extracts filesystem components usingPath::components().
How to check the behavior locally
The following reproduces the behavior on an isolated Windows test instance.
Create a file with a recognizable marker in PowerShell:
1
2
3
New-Item -ItemType Directory -Force 'C:\ferron-test\www\private'
Set-Content -Path 'C:\ferron-test\www\private\secret.txt' `
-Value 'FERRON_PRIVATE_FILE_MARKER'
Start Ferron with the option disabled and the location rule in place:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
http {
url_reject_backslash false
}
}
*:8080 {
root "C:/ferron-test/www"
location /private {
status 403
}
}
First request the file using ordinary separators. The expected response is 403 Forbidden:
1
curl.exe -i --path-as-is 'http://127.0.0.1:8080/private/secret.txt'
Then send the encoded-backslash form:
1
curl.exe -i --path-as-is 'http://127.0.0.1:8080/private%5Csecret.txt'
This returns 200 OK and the marker, bypassing the restrictive /private location.
For comparison, restore url_reject_backslash true, restart Ferron, and repeat the encoded request. The expected result with the default protection enabled is 400 Bad Request.
What operators can do
If an application does not need backslashes in URL paths, keep url_reject_backslash enabled. That preserves Ferron’s default rejection behavior and avoids this particular input reaching later path-processing stages.
If compatibility requires disabling it, review the whole request path from URL parsing through routing and filesystem access. In particular, do not rely on a location rule as the only protection for a sensitive file unless the rule and the filesystem resolver interpret separators consistently. Keep sensitive files outside publicly served roots where possible, and enforce authorization in a layer that applies to the resource regardless of how its path was spelled.
Operators should also check whether a reverse proxy, framework, or backend decodes or normalizes paths differently. A proxy’s acceptance of a request does not prove that each downstream component will agree about its meaning. Test encoded and literal separators against the actual deployed chain, especially on Windows.
Impact
The impact is disclosure of a file beneath the web root that is protected only by a location rule that routing fails to match. The scenario requires Windows, url_reject_backslash false, and a file otherwise available to the static-file handler.