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/liturgy/fetch_test.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/liturgy/fetch_test.go')
| -rw-r--r-- | internal/liturgy/fetch_test.go | 28 |
1 files changed, 28 insertions, 0 deletions
diff --git a/internal/liturgy/fetch_test.go b/internal/liturgy/fetch_test.go index ae8a68c..1cebb86 100644 --- a/internal/liturgy/fetch_test.go +++ b/internal/liturgy/fetch_test.go @@ -4,6 +4,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "testing" ) @@ -30,3 +31,30 @@ func TestLoadCaches(t *testing.T) { t.Error("cache returned different section count") } } + +// TestLoadRejectsInvalidDate is the liturgy-layer defense-in-depth check for +// the ?date= path-traversal finding: Load must reject a non-YYYY-MM-DD date +// before it ever builds a filesystem path from it, so every caller (web, +// cli, tui) is protected even if a future caller forgets to validate. +// +// The planted "passwd.json" sits one level *above* cacheDir() -- reachable +// only via a "../" date -- so if Load ever built jsonPath from the raw date +// unchecked, loadJSONCache would read it back and return its section instead +// of an error. +func TestLoadRejectsInvalidDate(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CACHE_HOME", dir) + + evilPath := filepath.Join(dir, "passwd.json") + if err := os.WriteFile(evilPath, []byte(`[{"Heading":"SHOULD-NEVER-BE-READ"}]`), 0o644); err != nil { + t.Fatal(err) + } + + secs, err := Load(Options{Date: "../passwd"}) + if err == nil { + t.Fatalf("Load(Date=%q) = (%v, nil), want a non-nil error", "../passwd", secs) + } + if secs != nil { + t.Errorf("Load(Date=%q) sections = %v, want nil", "../passwd", secs) + } +} |
