Priority
High — local file disclosure and path-containment correctness
Context
gowdk serve, the static development handler, and the live-reload file handler serve generated files directly from a user-selected output directory.
The current path flow in cmd/gowdk/serve.go is effectively:
rel := strings.TrimPrefix(path.Clean("/"+candidate), "/")
filePath := filepath.Join(root, filepath.FromSlash(rel))
relative, err := filepath.Rel(root, filePath)
// reject lexical ../ escape
info, err := os.Stat(filePath)
// later: http.ServeFile(..., filePath)
The filepath.Rel check proves only lexical containment. os.Stat and http.ServeFile follow filesystem links. An intermediate directory symlink, a leaf symlink, or a Windows reparse-point/junction inside the selected output root can therefore resolve to a file outside that root.
The default address is loopback, but --addr allows the server to bind to other interfaces. A generated output directory may also contain copied or user-created assets that the caller did not expect to expose outside the declared root.
Generated apps that serve an embedded fs.FS are not the primary concern here; this issue is about disk-backed serving paths.
Problem
A path such as:
dist/site/public-link -> /home/user/private
can remain lexically inside dist/site while resolving outside it. A request for /public-link/file.txt may therefore expose /home/user/private/file.txt.
Related failure modes include:
- a leaf symlink such as
dist/site/config.json -> ../../config.json;
- an intermediate symlinked directory;
- Windows directory junctions or other reparse points;
- a link being replaced between validation and open, creating a time-of-check/time-of-use race;
- inconsistent behavior between
gowdk serve, gowdk dev, and gowdk preview --hot when they share the disk-backed handler.
This should fail closed even when the output directory is considered developer-owned. The server contract says it serves the selected directory, not arbitrary files reachable through links from that directory.
Goal
Guarantee that every disk-backed file opened by GOWDK's serving commands is a regular file contained by the selected, resolved output root according to a documented symlink policy.
Proposed direction
1. Centralize rooted file opening
Introduce one helper/package used by all disk-backed serving paths, for example:
type RootedFiles struct {
// unexported resolved root and policy
}
func OpenRooted(root string, requestPath string) (fs.File, fs.FileInfo, error)
The helper should own:
- URL-path normalization;
- directory-index candidate selection;
- sensitive generated-file policy;
- root containment;
- symlink/reparse-point handling;
- regular-file validation;
- consistent not-found versus internal-error classification.
Do not leave each server mode to reproduce its own lexical check.
2. Reject filesystem links by default
The safest default for generated output is to reject any candidate whose path contains a symlink, junction, or equivalent filesystem indirection.
On platforms where race-resistant APIs are available, open path components relative to an already-open root directory and use no-follow semantics. On other platforms, combine Lstat/resolved-root validation with the strongest available checks and document residual platform limitations.
A simple filepath.EvalSymlinks check alone improves containment but does not eliminate replacement races. The implementation should prefer handle-relative/open-relative APIs where practical rather than claiming race resistance that it does not provide.
3. Make any link-following behavior explicit
If legitimate symlinked generated assets need support later, add an explicit policy such as --follow-symlinks rather than silently following them. Even in that mode, the final resolved path must remain inside the resolved root.
No link should ever permit an escape outside the selected root.
4. Preserve normal URL behavior
The fix should retain:
/ and trailing-slash index resolution;
- extensionless route lookup;
- GET/HEAD-only serving;
- live-reload HTML injection;
- generated endpoint guidance for unsupported methods;
- 404 behavior for unavailable files;
- Windows and Unix path support.
Unsafe or escaping paths should return a generic 404 rather than disclose host filesystem structure.
5. Cover all disk-backed entrypoints
Audit and migrate at least:
gowdk serve;
- static
gowdk dev serving;
gowdk preview and preview --hot;
- live-reload disk file reads;
- any testkit or future disk-output server that reuses
outputFilePath semantics.
Security considerations
- Error responses and logs must not reveal the resolved external target path to remote clients.
- Root resolution should happen once at startup where possible, while per-request checks should use that stable root identity.
- The implementation must consider Windows junctions/reparse points, not only Unix symbolic links.
- The server should not trust a lexical
filepath.Rel result as proof of filesystem containment.
- Tests should avoid requiring elevated symlink privileges where Windows policy prevents ordinary symlink creation; junction/reparse behavior can use platform-specific fixtures or focused unit boundaries.
Test plan
Add platform-neutral and platform-specific tests covering:
- normal file and directory-index serving;
- lexical
.. attempts;
- URL-encoded traversal attempts after normal HTTP URL parsing;
- leaf symlink to a file outside the root;
- intermediate symlinked directory outside the root;
- symlink resolving to a file still inside the root, according to the selected default policy;
- broken symlink;
- link loop;
- replacement of a checked path before open where a race-resistant abstraction can be tested;
- non-regular files such as sockets, devices, and named pipes;
- Windows junction/reparse-point containment;
gowdk serve, dev static serving, and live-reload behavior using the same rooted opener;
- generic client-facing 404 without external target disclosure.
Include a regression test demonstrating that a request cannot retrieve a sentinel file created outside the served root through a link inside the root.
Acceptance criteria
Non-goals
- Turning
gowdk serve into a production web server.
- Serving arbitrary source directories outside generated output.
- Adding a general virtual filesystem or mount system.
- Changing embedded generated-app file serving, except where shared policy code is intentionally reused.
Related
Priority
High — local file disclosure and path-containment correctness
Context
gowdk serve, the static development handler, and the live-reload file handler serve generated files directly from a user-selected output directory.The current path flow in
cmd/gowdk/serve.gois effectively:The
filepath.Relcheck proves only lexical containment.os.Statandhttp.ServeFilefollow filesystem links. An intermediate directory symlink, a leaf symlink, or a Windows reparse-point/junction inside the selected output root can therefore resolve to a file outside that root.The default address is loopback, but
--addrallows the server to bind to other interfaces. A generated output directory may also contain copied or user-created assets that the caller did not expect to expose outside the declared root.Generated apps that serve an embedded
fs.FSare not the primary concern here; this issue is about disk-backed serving paths.Problem
A path such as:
can remain lexically inside
dist/sitewhile resolving outside it. A request for/public-link/file.txtmay therefore expose/home/user/private/file.txt.Related failure modes include:
dist/site/config.json -> ../../config.json;gowdk serve,gowdk dev, andgowdk preview --hotwhen they share the disk-backed handler.This should fail closed even when the output directory is considered developer-owned. The server contract says it serves the selected directory, not arbitrary files reachable through links from that directory.
Goal
Guarantee that every disk-backed file opened by GOWDK's serving commands is a regular file contained by the selected, resolved output root according to a documented symlink policy.
Proposed direction
1. Centralize rooted file opening
Introduce one helper/package used by all disk-backed serving paths, for example:
The helper should own:
Do not leave each server mode to reproduce its own lexical check.
2. Reject filesystem links by default
The safest default for generated output is to reject any candidate whose path contains a symlink, junction, or equivalent filesystem indirection.
On platforms where race-resistant APIs are available, open path components relative to an already-open root directory and use no-follow semantics. On other platforms, combine
Lstat/resolved-root validation with the strongest available checks and document residual platform limitations.A simple
filepath.EvalSymlinkscheck alone improves containment but does not eliminate replacement races. The implementation should prefer handle-relative/open-relative APIs where practical rather than claiming race resistance that it does not provide.3. Make any link-following behavior explicit
If legitimate symlinked generated assets need support later, add an explicit policy such as
--follow-symlinksrather than silently following them. Even in that mode, the final resolved path must remain inside the resolved root.No link should ever permit an escape outside the selected root.
4. Preserve normal URL behavior
The fix should retain:
/and trailing-slash index resolution;Unsafe or escaping paths should return a generic 404 rather than disclose host filesystem structure.
5. Cover all disk-backed entrypoints
Audit and migrate at least:
gowdk serve;gowdk devserving;gowdk previewandpreview --hot;outputFilePathsemantics.Security considerations
filepath.Relresult as proof of filesystem containment.Test plan
Add platform-neutral and platform-specific tests covering:
..attempts;gowdk serve, dev static serving, and live-reload behavior using the same rooted opener;Include a regression test demonstrating that a request cannot retrieve a sentinel file created outside the served root through a link inside the root.
Acceptance criteria
filepath.Relcontainment.Non-goals
gowdk serveinto a production web server.Related