fix(protocol): reject unsafe catalogue text
- commit
- 4bb15ab67b7acb7ca0f6208892f411fdd415dc47
- author
- brice · · 2026-08-28 08:59 UTC
- committer
- brice · · 2026-08-28 14:56 UTC
- parent
- ca64e93
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +22 -2
M internal/protocol/catalogue/catalogue.go +1 -1
| @@ -246,7 +246,7 @@ func validText(value string) bool { | |||
| 246 | 246 | return false | |
| 247 | 247 | } | |
| 248 | 248 | for _, r := range value { | |
| 249 | - | if unicode.IsControl(r) { | |
| 249 | + | if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' { | |
| 250 | 250 | return false | |
| 251 | 251 | } | |
| 252 | 252 | } | |
| @@ -246,7 +246,7 @@ func validText(value string) bool { | |||||
| 246 | return false | 246 | return false | ||
| 247 | } | 247 | } | ||
| 248 | for _, r := range value { | 248 | for _, r := range value { | ||
| 249 | - | if unicode.IsControl(r) { | 249 | + | if unicode.IsControl(r) || unicode.Is(unicode.Bidi_Control, r) || r == '\u2028' || r == '\u2029' { |
| 250 | return false | 250 | return false | ||
| 251 | } | 251 | } | ||
| 252 | } | 252 | } | ||
M internal/protocol/catalogue/catalogue_test.go +20 -0
| @@ -41,6 +41,26 @@ func TestRemoteCatalogStateContract(t *testing.T) { | |||
| 41 | 41 | require.Equal(t, RemoteCatalogSchemaVersion, mismatch.Want) | |
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | + | func TestValidateRemoteCatalogRejectsTerminalUnsafeText(t *testing.T) { | |
| 45 | + | for _, tt := range []struct{ name, value string }{ | |
| 46 | + | {name: "bidi override", value: "\u202eoverride"}, | |
| 47 | + | {name: "line separator", value: "\u2028line separator"}, | |
| 48 | + | {name: "paragraph separator", value: "\u2029paragraph separator"}, | |
| 49 | + | } { | |
| 50 | + | t.Run(tt.name, func(t *testing.T) { | |
| 51 | + | err := ValidateRemoteCatalog(RemoteCatalog{ | |
| 52 | + | ProtocolVersion: protocol.Version, | |
| 53 | + | SchemaVersion: RemoteCatalogSchemaVersion, | |
| 54 | + | Sessions: []RemoteCatalogSession{{ | |
| 55 | + | LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionUp, | |
| 56 | + | Tabs: []RemoteCatalogTab{{ID: "tab-1", Detail: tt.value}}, | |
| 57 | + | }}, | |
| 58 | + | }) | |
| 59 | + | require.ErrorIs(t, err, ErrInvalidRemoteCatalog) | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 44 | 64 | func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) { | |
| 45 | 65 | err := ValidateRemoteCatalog(RemoteCatalog{ | |
| 46 | 66 | ProtocolVersion: protocol.Version, | |
| @@ -41,6 +41,26 @@ func TestRemoteCatalogStateContract(t *testing.T) { | |||||
| 41 | require.Equal(t, RemoteCatalogSchemaVersion, mismatch.Want) | 41 | require.Equal(t, RemoteCatalogSchemaVersion, mismatch.Want) | ||
| 42 | } | 42 | } | ||
| 43 | 43 | ||||
| 44 | + | func TestValidateRemoteCatalogRejectsTerminalUnsafeText(t *testing.T) { | |||
| 45 | + | for _, tt := range []struct{ name, value string }{ | |||
| 46 | + | {name: "bidi override", value: "\u202eoverride"}, | |||
| 47 | + | {name: "line separator", value: "\u2028line separator"}, | |||
| 48 | + | {name: "paragraph separator", value: "\u2029paragraph separator"}, | |||
| 49 | + | } { | |||
| 50 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 51 | + | err := ValidateRemoteCatalog(RemoteCatalog{ | |||
| 52 | + | ProtocolVersion: protocol.Version, | |||
| 53 | + | SchemaVersion: RemoteCatalogSchemaVersion, | |||
| 54 | + | Sessions: []RemoteCatalogSession{{ | |||
| 55 | + | LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionUp, | |||
| 56 | + | Tabs: []RemoteCatalogTab{{ID: "tab-1", Detail: tt.value}}, | |||
| 57 | + | }}, | |||
| 58 | + | }) | |||
| 59 | + | require.ErrorIs(t, err, ErrInvalidRemoteCatalog) | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | ||||
| 44 | func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) { | 64 | func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) { | ||
| 45 | err := ValidateRemoteCatalog(RemoteCatalog{ | 65 | err := ValidateRemoteCatalog(RemoteCatalog{ | ||
| 46 | ProtocolVersion: protocol.Version, | 66 | ProtocolVersion: protocol.Version, | ||
M internal/protocol/wire/wire_final_test.go +1 -1
| @@ -158,7 +158,7 @@ func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) { | |||
| 158 | 158 | {name: "size zero", mutate: func(b []byte) { binary.BigEndian.PutUint16(b[40:42], 0) }}, | |
| 159 | 159 | {name: "full flag false for reset", mutate: func(b []byte) { b[44] = 0 }}, | |
| 160 | 160 | {name: "invalid bool", mutate: func(b []byte) { b[44] = 2 }}, | |
| 161 | - | {name: "impossible data length", mutate: func(b []byte) { binary.BigEndian.PutUint32(b[50:54], ^uint32(0)) }}, | |
| 161 | + | {name: "impossible data length", mutate: func(b []byte) { binary.BigEndian.PutUint32(b[46:50], ^uint32(0)) }}, | |
| 162 | 162 | } { | |
| 163 | 163 | t.Run(tt.name, func(t *testing.T) { | |
| 164 | 164 | bad := append([]byte(nil), payload...) | |
| @@ -158,7 +158,7 @@ func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) { | |||||
| 158 | {name: "size zero", mutate: func(b []byte) { binary.BigEndian.PutUint16(b[40:42], 0) }}, | 158 | {name: "size zero", mutate: func(b []byte) { binary.BigEndian.PutUint16(b[40:42], 0) }}, | ||
| 159 | {name: "full flag false for reset", mutate: func(b []byte) { b[44] = 0 }}, | 159 | {name: "full flag false for reset", mutate: func(b []byte) { b[44] = 0 }}, | ||
| 160 | {name: "invalid bool", mutate: func(b []byte) { b[44] = 2 }}, | 160 | {name: "invalid bool", mutate: func(b []byte) { b[44] = 2 }}, | ||
| 161 | - | {name: "impossible data length", mutate: func(b []byte) { binary.BigEndian.PutUint32(b[50:54], ^uint32(0)) }}, | 161 | + | {name: "impossible data length", mutate: func(b []byte) { binary.BigEndian.PutUint32(b[46:50], ^uint32(0)) }}, |
| 162 | } { | 162 | } { | ||
| 163 | t.Run(tt.name, func(t *testing.T) { | 163 | t.Run(tt.name, func(t *testing.T) { | ||
| 164 | bad := append([]byte(nil), payload...) | 164 | bad := append([]byte(nil), payload...) | ||