sumdb: ignore unrelated hashes in Lookup During Lookup, if the server returns a validly signed record for an unrelated module which contains in the signed tree head another hash for the module which was requested in the "extension" portion of the signed tree head (anything following the tree hash, but before the signatures), _only_ the unrelated hashes are validated by checking the relevant tiles, ignoring the portion in the tree head. Lookup would then look for hash lines which match the requested module path in the fully returned record, not just the validated portion, causing only the hash appended to the tree head portion to be returned. This would allow a malicious sumdb instance to return a validly signed response for a module from which Lookup would return a hash which is not actually present in the tree. This would allow a conspiring malicious sumdb and module proxy (or a module proxy which is proxying the sumdb _and has the sumdb key_) to serve malicious module content which is not recorded in the transparency log. The fix is quite simple, instead of caching the full record, we only cache the hash lines returned by tlog.ParseRecord, which are validated. Lookup will then only extract matching hashes from the validated portion, instead of the full record. Thanks to Mundur (https://github.com/M0nd0R) for reporting this issue. Fixes CVE-2026-56864 Fixes golang/go#80745 Change-Id: I4a418620e8e4e8ab5f7ec41b0ed443a92e9be6e1 Reviewed-on: https://go-review.googlesource.com/c/mod/+/815000 Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Auto-Submit: Gopher Robot <gobot@golang.org> Reviewed-by: Neal Patel <nealpatel@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/sumdb/client.go b/sumdb/client.go index 47533a8..25df2cd 100644 --- a/sumdb/client.go +++ b/sumdb/client.go
@@ -274,7 +274,7 @@ c.ops.WriteCache(file, data) } - return cached{data, nil} + return cached{text, nil} }).(cached) if result.err != nil { return nil, result.err
diff --git a/sumdb/client_test.go b/sumdb/client_test.go index f085e7d..b884d60 100644 --- a/sumdb/client_test.go +++ b/sumdb/client_test.go
@@ -189,6 +189,55 @@ } } +func TestRejectUnauthenticatedLines(t *testing.T) { + tc := newTestClient(t) + + data := "golang.org/x/good v1.0.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=\n" + id := tc.treeSize + tc.treeSize++ + rec, err := tlog.FormatRecord(id, []byte(data)) + if err != nil { + t.Fatal(err) + } + hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash([]byte(data)), tc) + if err != nil { + t.Fatal(err) + } + tc.hashes = append(tc.hashes, hashes...) + + // Create lookup result. + h, err := tlog.TreeHash(tc.treeSize, tc) + if err != nil { + t.Fatal(err) + } + text := append(tlog.FormatTree(tlog.Tree{N: tc.treeSize, Hash: h}), []byte("golang.org/x/bad v1.0.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=\n")...) + signed, err := note.Sign(¬e.Note{Text: string(text)}, tc.signer) + if err != nil { + t.Fatal(err) + } + + lookupRes := append(rec, signed...) + tc.remote["/lookup/golang.org/x/bad@v1.0.0"] = lookupRes + + // Create new tiles. + tiles := tlog.NewTiles(tc.tileHeight, id, tc.treeSize) + for _, tile := range tiles { + data, err := tlog.ReadTileData(tile, tc) + if err != nil { + t.Fatal(err) + } + tc.remote["/"+tile.Path()] = data + } + + lines, err := tc.client.Lookup("golang.org/x/bad", "v1.0.0") + if err != nil { + t.Fatal(err) + } + if len(lines) != 0 { + t.Errorf("Lookup(%q): expected no hashes, got %q", "golang.org/x/bad", strings.Join(lines, "\n")) + } +} + // A testClient is a self-contained client-side testing environment. type testClient struct { t *testing.T // active test