summaryrefslogtreecommitdiff
path: root/internal/web/server_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/server_test.go')
-rw-r--r--internal/web/server_test.go53
1 files changed, 52 insertions, 1 deletions
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index 0268d3b..30c8fbf 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"os"
+ "path/filepath"
"strings"
"testing"
@@ -25,7 +26,8 @@ func TestServer(t *testing.T) {
}))
defer fixtureServer.Close()
liturgy.SetBaseURL(fixtureServer.URL + "/liturgia/%s/Ewangelia")
- t.Setenv("XDG_CACHE_HOME", t.TempDir())
+ cacheHome := t.TempDir()
+ t.Setenv("XDG_CACHE_HOME", cacheHome)
srv := NewServer(config.Default())
@@ -196,6 +198,55 @@ func TestServer(t *testing.T) {
t.Errorf("body missing display select: %q", rec.Body.String())
}
})
+
+ // Regression coverage for the ?date= path-traversal finding: resolveQuery
+ // must reject anything that isn't YYYY-MM-DD and fall back to today(),
+ // the same "normalize, don't trust" pattern requestDisplay already uses.
+ t.Run("date path traversal does not read a planted cache file", func(t *testing.T) {
+ // cacheDir() == filepath.Join(cacheHome, "lectio"), so
+ // filepath.Join(cacheDir(), "../evil"+".json") resolves to
+ // cacheHome/evil.json -- one level *above* the real cache dir, and
+ // only reachable via an unvalidated "../" date. If the marker below
+ // ever appears in a response, liturgy.Load read this planted file.
+ evilPath := filepath.Join(cacheHome, "evil.json")
+ evilJSON := `[{"Heading":"LEAKED-VIA-TRAVERSAL","PartID":"ewangelia","Paragraphs":[["s"]]}]`
+ if err := os.WriteFile(evilPath, []byte(evilJSON), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ baseline := httptest.NewRecorder()
+ srv.ServeHTTP(baseline, httptest.NewRequest("GET", "/readings?v=wuj", nil)) // no date -> today()
+ if baseline.Code != http.StatusOK {
+ t.Fatalf("baseline status = %d, want 200", baseline.Code)
+ }
+
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, httptest.NewRequest("GET", "/readings?date=../evil&v=wuj", nil))
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", rec.Code)
+ }
+ body := rec.Body.String()
+ if strings.Contains(body, "LEAKED-VIA-TRAVERSAL") {
+ t.Fatalf("traversal date reached the planted cache file outside the cache dir: %q", body)
+ }
+ if body != baseline.Body.String() {
+ t.Errorf("traversal date did not fall back to today() identically to omitting date\n got: %q\nwant: %q", body, baseline.Body.String())
+ }
+ })
+
+ t.Run("date query with many ../ segments falls back to today, same as omitting date", func(t *testing.T) {
+ baseline := httptest.NewRecorder()
+ srv.ServeHTTP(baseline, httptest.NewRequest("GET", "/readings?v=wuj", nil)) // no date -> today()
+
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, httptest.NewRequest("GET", "/readings?date=../../../../etc/hostname&v=wuj", nil))
+ if rec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200", rec.Code)
+ }
+ if rec.Body.String() != baseline.Body.String() {
+ t.Errorf("traversal-shaped date did not behave identically to omitting date\n got: %q\nwant: %q", rec.Body.String(), baseline.Body.String())
+ }
+ })
}
// TestChooseListener exercises chooseListener's port-selection logic