diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 15:49:22 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 15:49:22 +0200 |
| commit | 73d00113f5ed5fe99e365d2053dc70a6da8a81ee (patch) | |
| tree | cb43b29be24768bf4a8f4baa5590664e279e7fb9 /internal/web/server.go | |
| parent | 905b7473dd3987928e9ac9c81405548ba7be4c8d (diff) | |
| download | lectio-73d00113f5ed5fe99e365d2053dc70a6da8a81ee.tar.gz lectio-73d00113f5ed5fe99e365d2053dc70a6da8a81ee.zip | |
web,liturgy: validate date against path traversal; bind lectio-web to localhost
An unvalidated ?date= query param flowed straight into
liturgy.Load's filepath.Join(dir, date+".json"/".html") before any
network call, letting a crafted date (e.g. "../../../../etc/hostname")
read an arbitrary file whose JSON, if present, unmarshals into
[]liturgy.Section and renders back to the client. Fix both layers:
resolveQuery now falls back to today() on empty or non-YYYY-MM-DD
date (mirroring requestDisplay's normalize-don't-trust pattern), and
liturgy.Load itself rejects a non-matching date before building any
cache path, protecting every caller even if a future one forgets to
validate.
Also bind lectio-web's listener to 127.0.0.1 instead of all
interfaces: it is a personal tool whose Run already prints
http://localhost:<port>, so it should not be reachable from the LAN.
Diffstat (limited to 'internal/web/server.go')
| -rw-r--r-- | internal/web/server.go | 25 |
1 files changed, 18 insertions, 7 deletions
diff --git a/internal/web/server.go b/internal/web/server.go index 5886f0f..3fe1e4e 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -14,6 +14,7 @@ import ( "net" "net/http" "os/exec" + "regexp" "runtime" "strings" "time" @@ -57,6 +58,13 @@ func today() string { return time.Now().Format("2006-01-02") } +// dateRe validates a ?date= query param before it is ever handed to +// readings.Load/liturgy.Load, which build a filesystem cache path by string +// concatenation from it -- an unvalidated date is a path-traversal vector. +// Compiled once at package scope (not per request), the same shape as +// internal/cli's dateRe. See resolveQuery. +var dateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) + // shiftDate adds days to date (YYYY-MM-DD); an unparsable date is returned // unchanged, mirroring internal/tui's shiftDate. func shiftDate(date string, days int) string { @@ -119,7 +127,7 @@ func requestDisplay(cfg config.Config, r *http.Request) string { // can't drift. func resolveQuery(cfg config.Config, r *http.Request) (date, lectionary string, all bool, versions []string, display string) { date = r.URL.Query().Get("date") - if date == "" { + if date == "" || !dateRe.MatchString(date) { date = today() } lectionary = requestLectionary(cfg, r) @@ -328,17 +336,20 @@ func themeCSSHandler(cfg config.Config) http.HandlerFunc { // defaultWebPort is the port chooseListener prefers when cfg.WebPort is 0. const defaultWebPort = 1099 -// chooseListener binds the port to serve on. port==0 means "prefer -// defaultWebPort (1099), else let the OS pick a free port"; a non-zero port -// is bound exactly (and its bind error surfaced if the port is in use). +// chooseListener binds the port to serve on, on loopback only (127.0.0.1) -- +// lectio-web is documented as a personal tool and Run prints an +// http://localhost/... URL, so it must not be reachable from the LAN. port==0 +// means "prefer defaultWebPort (1099), else let the OS pick a free port"; a +// non-zero port is bound exactly (and its bind error surfaced if the port is +// in use). func chooseListener(port int) (net.Listener, error) { if port != 0 { - return net.Listen("tcp", fmt.Sprintf(":%d", port)) + return net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) } - if ln, err := net.Listen("tcp", fmt.Sprintf(":%d", defaultWebPort)); err == nil { + if ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", defaultWebPort)); err == nil { return ln, nil } - return net.Listen("tcp", ":0") // 1099 taken -> any free port + return net.Listen("tcp", "127.0.0.1:0") // 1099 taken -> any free port } // Run starts lectio-web: listens on cfg.WebPort via chooseListener (0 |
