fix(protocol): enforce encoded catalogue limit
- commit
- 1667cd9080a0a864368676504d8415d406ca9932
- author
- brice · · 2026-08-28 16:22 UTC
- parent
- 7295b7b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +41 -0
M internal/protocol/catalogue/catalogue.go +9 -0
| @@ -2,6 +2,7 @@ | |||
| 2 | 2 | package catalogue | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | + | "encoding/json" | |
| 5 | 6 | "errors" | |
| 6 | 7 | "fmt" | |
| 7 | 8 | "math" | |
| @@ -213,6 +214,14 @@ func ValidateRemoteCatalog(c RemoteCatalog) error { | |||
| 213 | 214 | if bytes > RemoteCatalogMaxCatalogBytes { | |
| 214 | 215 | return ErrRemoteCatalogTooLarge | |
| 215 | 216 | } | |
| 217 | + | encoded, err := json.Marshal(c) | |
| 218 | + | if err != nil { | |
| 219 | + | return fmt.Errorf("%w: encoding catalogue: %v", ErrInvalidRemoteCatalog, err) | |
| 220 | + | } | |
| 221 | + | // marshalListing appends one newline after the JSON payload. | |
| 222 | + | if len(encoded)+1 > RemoteCatalogMaxCatalogBytes { | |
| 223 | + | return ErrRemoteCatalogTooLarge | |
| 224 | + | } | |
| 216 | 225 | return nil | |
| 217 | 226 | } | |
| 218 | 227 | ||
| @@ -2,6 +2,7 @@ | |||||
| 2 | package catalogue | 2 | package catalogue | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | + | "encoding/json" | |||
| 5 | "errors" | 6 | "errors" | ||
| 6 | "fmt" | 7 | "fmt" | ||
| 7 | "math" | 8 | "math" | ||
| @@ -213,6 +214,14 @@ func ValidateRemoteCatalog(c RemoteCatalog) error { | |||||
| 213 | if bytes > RemoteCatalogMaxCatalogBytes { | 214 | if bytes > RemoteCatalogMaxCatalogBytes { | ||
| 214 | return ErrRemoteCatalogTooLarge | 215 | return ErrRemoteCatalogTooLarge | ||
| 215 | } | 216 | } | ||
| 217 | + | encoded, err := json.Marshal(c) | |||
| 218 | + | if err != nil { | |||
| 219 | + | return fmt.Errorf("%w: encoding catalogue: %v", ErrInvalidRemoteCatalog, err) | |||
| 220 | + | } | |||
| 221 | + | // marshalListing appends one newline after the JSON payload. | |||
| 222 | + | if len(encoded)+1 > RemoteCatalogMaxCatalogBytes { | |||
| 223 | + | return ErrRemoteCatalogTooLarge | |||
| 224 | + | } | |||
| 216 | return nil | 225 | return nil | ||
| 217 | } | 226 | } | ||
| 218 | 227 | ||||
M internal/protocol/catalogue/catalogue_test.go +32 -0
| @@ -1,6 +1,9 @@ | |||
| 1 | 1 | package catalogue | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "fmt" | |
| 6 | + | "strings" | |
| 4 | 7 | "testing" | |
| 5 | 8 | "time" | |
| 6 | 9 | ||
| @@ -69,6 +72,35 @@ func TestValidateRemoteCatalogCacheEntriesRejectsTerminalUnsafeHost(t *testing.T | |||
| 69 | 72 | require.ErrorIs(t, err, ErrInvalidRemoteCatalog) | |
| 70 | 73 | } | |
| 71 | 74 | ||
| 75 | + | func TestValidateRemoteCatalogRejectsOversizedEncodedJSON(t *testing.T) { | |
| 76 | + | sessions := make([]RemoteCatalogSession, 4) | |
| 77 | + | for sessionIndex := range sessions { | |
| 78 | + | tabs := make([]RemoteCatalogTab, 100) | |
| 79 | + | for tabIndex := range tabs { | |
| 80 | + | tabs[tabIndex] = RemoteCatalogTab{ | |
| 81 | + | ID: fmt.Sprintf("tab-%d-%d", sessionIndex, tabIndex), | |
| 82 | + | Index: uint16(tabIndex), | |
| 83 | + | Detail: strings.Repeat("<", RemoteCatalogMaxDetailBytes), | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | sessions[sessionIndex] = RemoteCatalogSession{ | |
| 87 | + | LifecycleID: [16]byte{byte(sessionIndex + 1)}, | |
| 88 | + | Name: fmt.Sprintf("work-%d", sessionIndex), | |
| 89 | + | State: RemoteCatalogSessionUp, | |
| 90 | + | Tabs: tabs, | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | catalog := RemoteCatalog{ | |
| 94 | + | ProtocolVersion: protocol.Version, | |
| 95 | + | SchemaVersion: RemoteCatalogSchemaVersion, | |
| 96 | + | Sessions: sessions, | |
| 97 | + | } | |
| 98 | + | encoded, err := json.Marshal(catalog) | |
| 99 | + | require.NoError(t, err) | |
| 100 | + | require.Greater(t, len(encoded)+1, RemoteCatalogMaxCatalogBytes) | |
| 101 | + | require.ErrorIs(t, ValidateRemoteCatalog(catalog), ErrRemoteCatalogTooLarge) | |
| 102 | + | } | |
| 103 | + | ||
| 72 | 104 | func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) { | |
| 73 | 105 | err := ValidateRemoteCatalog(RemoteCatalog{ | |
| 74 | 106 | ProtocolVersion: protocol.Version, | |
| @@ -1,6 +1,9 @@ | |||||
| 1 | package catalogue | 1 | package catalogue | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "encoding/json" | |||
| 5 | + | "fmt" | |||
| 6 | + | "strings" | |||
| 4 | "testing" | 7 | "testing" | ||
| 5 | "time" | 8 | "time" | ||
| 6 | 9 | ||||
| @@ -69,6 +72,35 @@ func TestValidateRemoteCatalogCacheEntriesRejectsTerminalUnsafeHost(t *testing.T | |||||
| 69 | require.ErrorIs(t, err, ErrInvalidRemoteCatalog) | 72 | require.ErrorIs(t, err, ErrInvalidRemoteCatalog) | ||
| 70 | } | 73 | } | ||
| 71 | 74 | ||||
| 75 | + | func TestValidateRemoteCatalogRejectsOversizedEncodedJSON(t *testing.T) { | |||
| 76 | + | sessions := make([]RemoteCatalogSession, 4) | |||
| 77 | + | for sessionIndex := range sessions { | |||
| 78 | + | tabs := make([]RemoteCatalogTab, 100) | |||
| 79 | + | for tabIndex := range tabs { | |||
| 80 | + | tabs[tabIndex] = RemoteCatalogTab{ | |||
| 81 | + | ID: fmt.Sprintf("tab-%d-%d", sessionIndex, tabIndex), | |||
| 82 | + | Index: uint16(tabIndex), | |||
| 83 | + | Detail: strings.Repeat("<", RemoteCatalogMaxDetailBytes), | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | sessions[sessionIndex] = RemoteCatalogSession{ | |||
| 87 | + | LifecycleID: [16]byte{byte(sessionIndex + 1)}, | |||
| 88 | + | Name: fmt.Sprintf("work-%d", sessionIndex), | |||
| 89 | + | State: RemoteCatalogSessionUp, | |||
| 90 | + | Tabs: tabs, | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | catalog := RemoteCatalog{ | |||
| 94 | + | ProtocolVersion: protocol.Version, | |||
| 95 | + | SchemaVersion: RemoteCatalogSchemaVersion, | |||
| 96 | + | Sessions: sessions, | |||
| 97 | + | } | |||
| 98 | + | encoded, err := json.Marshal(catalog) | |||
| 99 | + | require.NoError(t, err) | |||
| 100 | + | require.Greater(t, len(encoded)+1, RemoteCatalogMaxCatalogBytes) | |||
| 101 | + | require.ErrorIs(t, ValidateRemoteCatalog(catalog), ErrRemoteCatalogTooLarge) | |||
| 102 | + | } | |||
| 103 | + | ||||
| 72 | func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) { | 104 | func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) { | ||
| 73 | err := ValidateRemoteCatalog(RemoteCatalog{ | 105 | err := ValidateRemoteCatalog(RemoteCatalog{ | ||
| 74 | ProtocolVersion: protocol.Version, | 106 | ProtocolVersion: protocol.Version, | ||