From e1aadf2128ccaf2ed027af0d14faa609b05427f7 Mon Sep 17 00:00:00 2001 From: Mara Nikola Kiefer <8320933+mnkiefer@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:49:23 +0100 Subject: [PATCH] Reject mismatched SQLite keyset continuation keys Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dashboardstore/conformance_test.go | 24 +++++++++++++++++++ server/internal/sqlitex/query.go | 8 ++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/server/internal/dashboardstore/conformance_test.go b/server/internal/dashboardstore/conformance_test.go index cc1359a88..bd86b9405 100644 --- a/server/internal/dashboardstore/conformance_test.go +++ b/server/internal/dashboardstore/conformance_test.go @@ -229,6 +229,30 @@ func readConformance(t *testing.T, fixture readFixture, backend string) { t.Fatal(err) } }) + t.Run("keyset-continuation-keys", func(t *testing.T) { + definitions := []query.Definition{{ + Name: "subjects", From: "$workflows", + Select: []query.SelectedField{{Field: "repositoryId"}, {Field: "path"}, {Field: "id"}}, + OrderBy: []query.OrderField{{Field: "repositoryId"}, {Field: "path"}, {Field: "id"}}, + Page: &query.PageBoundary{ + Keys: []string{"repositoryId", "path", "id"}, Revision: "workflow-subjects", Comparator: "utf8-bytewise-v1", + }, + }, {Name: "visible", From: "subjects"}} + err := store.WithReadTransaction(t.Context(), func(ctx context.Context, reader dashboardstore.Reader) error { + _, _, err := reader.ExecuteSQLPlanWithOptions(ctx, definitions, []string{"visible"}, dashboardstore.ExecutionOptions{ + Pages: map[string]dashboardstore.Page{"visible": {Limit: 1, Keyset: &dashboardstore.KeysetPage{ + Boundary: "subjects", Comparator: "utf8-bytewise-v1", Keys: []string{"id", "path", "repositoryId"}, + }}}, + }) + if err == nil || !strings.Contains(err.Error(), "invalid or stale continuation token") { + t.Fatalf("mismatched continuation keys were accepted: %v", err) + } + return nil + }) + if err != nil { + t.Fatal(err) + } + }) t.Run("cancellation", func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) cancel() diff --git a/server/internal/sqlitex/query.go b/server/internal/sqlitex/query.go index b9f4646f1..2a08f4001 100644 --- a/server/internal/sqlitex/query.go +++ b/server/internal/sqlitex/query.go @@ -65,9 +65,15 @@ func (r *reader) ExecuteSQLPlanWithOptions(ctx context.Context, definitions []qu if err != nil { return nil, metrics, err } - if plan == nil || plan.Boundary != page.Keyset.Boundary || plan.Comparator != page.Keyset.Comparator { + if plan == nil || plan.Boundary != page.Keyset.Boundary || + plan.Comparator != page.Keyset.Comparator || len(plan.Keys) != len(page.Keyset.Keys) { return nil, metrics, fmt.Errorf("invalid or stale continuation token for %q", output) } + for index, key := range plan.Keys { + if key.Output != page.Keyset.Keys[index] { + return nil, metrics, fmt.Errorf("invalid or stale continuation token for %q", output) + } + } keysetPage = &page } executionDefinitions, fields := projectSnapshotPlan(definitions, requested, order)