ParseConnBlob dereferences the elements of the region and node arrays without a nil check. CBOR nulls decode to nil pointers, so a blob with null in either array crashes the process.
Reproducer
func TestNilRegion(t *testing.T) {
b, _ := cbor.Marshal(map[string]any{
"p": key.NewNode().Public().AppendTo(nil),
"r": []any{nil},
})
ParseConnBlob(ConnBlob("tc" + base64.RawURLEncoding.EncodeToString(b)))
}
panic: runtime error: invalid memory address or nil pointer dereference
github.com/tailscale/tailcat.(*wireRegion).derpRegion(...)
wire.go:94
github.com/tailscale/tailcat.ParseConnBlob(...)
tailcat.go:849
The same happens one level down with {"r": [{"i": 1, "N": [null]}]}, which panics at wire.go:100 on a nil *wireNode.
Where
ParseConnBlob calls wr.derpRegion() on each element of w.Region (tailcat.go:848-850). derpRegion is a pointer method that reads w.RegionID immediately, and it then ranges over w.Nodes reading n.Name and friends. Neither loop checks for nil.
Why it matters
Blobs are attacker-influenced. addrBlobArg treats a CLI argument as a DNS name when it is not a tc-prefixed blob, looks up its TXT records, and takes the tailcat= value as the blob (cmd/tailcat/tailcat.go:472-490). So the owner of a DNS name — or anyone who can get a blob pasted into a command — can crash the client.
This is the same class of bug as #26, which rejected malformed public keys in NodePublic.UnmarshalBinary for the same reason. That change validated the key lengths, but the array elements were left unchecked.
Callers cannot defend against it: both call sites in cmd/tailcat probe with ParseConnBlob and test err == nil, which a panic bypasses.
ParseConnBlobRaw is not affected — it returns the wire form without the conversion, so tailcat parse prints "Region": [null] rather than crashing.
Version
main at 1fba818, go1.27.0.
I can send a PR with a fix and a test that fails before the change.
ParseConnBlobdereferences the elements of the region and node arrays without a nil check. CBOR nulls decode to nil pointers, so a blob withnullin either array crashes the process.Reproducer
The same happens one level down with
{"r": [{"i": 1, "N": [null]}]}, which panics atwire.go:100on a nil*wireNode.Where
ParseConnBlobcallswr.derpRegion()on each element ofw.Region(tailcat.go:848-850).derpRegionis a pointer method that readsw.RegionIDimmediately, and it then ranges overw.Nodesreadingn.Nameand friends. Neither loop checks for nil.Why it matters
Blobs are attacker-influenced.
addrBlobArgtreats a CLI argument as a DNS name when it is not atc-prefixed blob, looks up its TXT records, and takes thetailcat=value as the blob (cmd/tailcat/tailcat.go:472-490). So the owner of a DNS name — or anyone who can get a blob pasted into a command — can crash the client.This is the same class of bug as #26, which rejected malformed public keys in
NodePublic.UnmarshalBinaryfor the same reason. That change validated the key lengths, but the array elements were left unchecked.Callers cannot defend against it: both call sites in
cmd/tailcatprobe withParseConnBloband testerr == nil, which a panic bypasses.ParseConnBlobRawis not affected — it returns the wire form without the conversion, sotailcat parseprints"Region": [null]rather than crashing.Version
mainat 1fba818, go1.27.0.I can send a PR with a fix and a test that fails before the change.