Repository navigation
Conversation
Adds a built-in read-only file server origin, so a local directory can be
shared through a tunnel without running a separate web server:
cloudflared tunnel --file ./public
The same origin is available from a configuration file:
ingress:
- hostname: files.example.com
service: file:/abs/path
The origin never listens on a public interface: cloudflared starts an
in-process HTTP server on 127.0.0.1:0 and points the embedded httpService
at it, the same way --hello-world does.
Hardening, deliberately different from http.FileServer defaults:
- GET / HEAD only, everything else 405
- no directory listings (directories are 404)
- dotfiles and dot directories are always 404
- symlinks are resolved and must stay inside the root
- X-Content-Type-Options: nosniff
- --file is mutually exclusive with --url and --unix-socket
Tests cover traversal (raw and percent-encoded), dotfiles, directory
listings, symlink escape, method filtering and invalid roots.
Refs cloudflare#611
- reject path components with trailing dots or spaces: Windows strips them, so "/a.txt." used to alias "a.txt" - set IdleTimeout on the in-process server so a slow client cannot pin a connection indefinitely - move the filesystem checks from parsing to start(), so validating a configuration file no longer requires the directory to exist on the machine doing the validation - print the resolved absolute directory in the quick tunnel banner instead of the raw flag value - add tests for CLI flag parsing, the config service form, flag exclusivity and the trailing-dot alias
hughcube
force-pushed
the
feature/serve-local-directory
branch
from
November 13, 2026 15:22
81f1b5d to
5be4cc2
Compare
added 3 commits
November 13, 2026 23:46
Red team review found that the segment checks validated the request path
while the filesystem resolved it differently:
- a backslash separates path components on Windows but is opaque to
path.Clean, so "/%5c.env" collapsed into a single segment and bypassed
both the dotfile rule and the ".." rule, serving .env and .git/config
from a directory that was supposed to hide them
- Windows 8.3 short names ("GIT~1") aliased dot directories without any
backslash involved
- reserved device names ("NUL") resolved successfully and are not regular
files
Fixes:
- reject any request path containing a backslash outright
- factor the rules into safeRelativePath and apply it to the request path
AND the resolved filesystem path, which catches short names and symlink
indirection
- reject Windows reserved device names
- require a per-instance random header on the loopback listener, so other
processes on the machine cannot read the directory by connecting to the
port directly; RoundTrip injects it for traffic that came via the tunnel
- only serve regular files
Tests cover every vector above, plus a request without the loopback header.
Second round of adversarial review found that the first round of tests could not actually fail: on Linux, deleting three of the four path defences left the suite green, because the payloads 404 anyway when the file does not exist. - move the path policy into safeRelativePath and test it directly, so every rule is pinned on every platform instead of relying on filesystem state - make safeRelativePath itself reject backslashes, removing the platform dependency from the rule (the HTTP layer still rejects them early) - add unit tests for reserved device names, which previously had no coverage - --file is now rejected together with --hello-world and --bastion too; before this it silently won because its check came first - call server.Close() on shutdown so keep-alive connections the shared transport may hold are torn down, and drop the write-only listener field - document that only "/" maps to index.html, so "/sub/" is a 404 by design Verified by mutation: deleting the backslash rule now fails TestSafeRelativePath.
filepath.Rel returns OS-separated paths, so rejecting every backslash also rejected nested files on Windows. Split the policy: safeRequestPath rejects backslashes for request-derived paths, safeRelativePath normalises and validates filesystem-derived paths.
hughcube
force-pushed
the
feature/serve-local-directory
branch
from
November 13, 2026 16:28
4f72281 to
a87c24d
Compare
Adversarial review, round three, ran mutation tests over every layer of the path policy. Two of them were provably dead and one comment was wrong about why a check exists: - the explicit backslash block in ServeHTTP tested the same predicate as safeRequestPath (path.Clean never produces or removes a backslash), so it is removed; the policy function keeps the rule - isWithin was fully subsumed by the resolved-path check, which rejects both a ".." escape and a symlink pointing outside the root; removed, and the remaining block now says that it is the containment check - the comment claimed the backslash rule stopped "/%5c.env" from reaching ".env". It does not: safeRelativePath maps the backslash through filepath.ToSlash, so that payload is rejected by the empty-segment rule. What the backslash rule actually buys is a single URL per file. Comment corrected rather than the code changed. New tests, both verified to fail when the defence they cover is removed: - a symlink inside the root pointing at a dotfile, which only the resolved check catches - a request through RoundTrip versus a direct hit on the loopback port; without this, deleting RoundTrip would keep the suite green while every real request returns 403
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a built-in, read-only file server origin, so a local directory can be served through a tunnel without starting a separate web server.
The same origin is available from a configuration file:
Refs #611.
Why
Sharing a directory today means running a separate static server (
python -m http.server,caddy file-server, ...) and pointing--urlat it. #611 asks for the ngrok-equivalentfile://origin.The change is small because cloudflared already hosts origins itself:
helloWorldstarts a listener and rewriteshttpService.url.fileServerdoes exactly the same, with a hardened file handler instead of a hello page.How
ingress/origin_file.go: newfileServerorigin. Onstart()it listens on127.0.0.1:0and points the embeddedhttpServiceat it. Nothing is bound to a public interface, all traffic still arrives through the existing tunnel.ingress/ingress.go: acceptservice: file:<dir>in configuration, and--fileon the CLI viaparseSingleOriginService.cmd/cloudflared/tunnel/{cmd,quick_tunnel}.go: register--file, let it trigger a quick tunnel, and show the served directory in the startup banner.Safety
Deliberately stricter than
http.FileServerdefaults:405404EvalSymlinksand required to stay inside the rootX-Content-Type-Options: nosniff--fileis mutually exclusive with--urland--unix-socketingress/origin_file_test.gocovers raw and percent-encoded traversal, dotfiles, directory listings, symlink escape, method filtering and invalid roots.Verified end to end on Windows:
cloudflared tunnel --file <dir>returns200for a file and404for.envover a quick tunnel.Deliberately not included
fileServerknobs inoriginRequest(index, deny list, cache headers, listing opt-in). Happy to add if the direction is accepted.\\\\server\\shareis currently treated like any other path.I understand this may be out of scope for cloudflared. If so, closing this without merge is fine, but the code is here if the direction ever changes.