test: fuzz bounded protocol parsers

This commit is contained in:
2026-09-11 08:21:24 +00:00
parent 418681d38e
commit f6f900e597
6 changed files with 132 additions and 14 deletions
+29
View File
@@ -42,6 +42,35 @@ func TestDecodeEnvelopeBoundaries_HP_PROTO_01(t *testing.T) {
}
}
func FuzzDecodeEnvelopeBounded_SEC_PROTO_01(f *testing.F) {
valid, err := proto.Marshal(&rvboxv1.AgentEnvelope{Payload: &rvboxv1.AgentEnvelope_ClientHello{ClientHello: validHello()}})
if err != nil {
f.Fatal(err)
}
f.Add(valid)
f.Add([]byte{0xff})
f.Fuzz(func(t *testing.T, data []byte) {
limits := DefaultLimits()
if len(data) > int(limits.MaxEnvelopeBytes)+1 {
return
}
_, _ = DecodeEnvelope(data, limits, rvboxv1.Platform_PLATFORM_WINDOWS)
})
}
func FuzzDecodeOutputChunkBounded_SEC_PROTO_02(f *testing.F) {
f.Add([]byte("plain"), uint8(rvboxv1.Compression_COMPRESSION_NONE), uint64(5))
f.Add([]byte{0x28, 0xb5, 0x2f, 0xfd}, uint8(rvboxv1.Compression_COMPRESSION_ZSTD), uint64(1))
f.Fuzz(func(t *testing.T, data []byte, compression uint8, rawBytes uint64) {
const limit = uint64(64 << 10)
if len(data) > int(limit) {
return
}
chunk := &rvboxv1.OutputChunk{Stream: rvboxv1.StreamKind_STREAM_STDOUT, Compression: rvboxv1.Compression(compression % 3), CompressedSize: uint64(len(data)), UncompressedSize: rawBytes % (limit + 2), Data: data}
_, _ = DecodeOutputChunk(chunk, limit)
})
}
func TestEnvelopeSessionFencingShape_BH_SES_01(t *testing.T) {
t.Parallel()
+26 -14
View File
@@ -55,20 +55,9 @@ func (handler *JSONRPCHandler) ServeHTTP(response http.ResponseWriter, request *
handler.writeRPCError(response, nil, jsonRPCParseError, "could not read request", nil)
return
}
if int64(len(body)) > handler.MaxBody {
handler.writeRPCError(response, nil, jsonRPCInvalid, "request body exceeds limit", nil)
return
}
var envelope jsonRPCRequest
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&envelope); err != nil {
handler.writeRPCError(response, nil, jsonRPCParseError, "invalid JSON", nil)
return
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF || envelope.JSONRPC != jsonRPCVersion || envelope.Method == "" || len(envelope.ID) == 0 || bytes.Equal(bytes.TrimSpace(envelope.ID), []byte("null")) {
handler.writeRPCError(response, nil, jsonRPCInvalid, "invalid JSON-RPC request", nil)
envelope, errorCode, errorMessage := decodeJSONRPCRequest(body, handler.MaxBody)
if errorCode != 0 {
handler.writeRPCError(response, nil, errorCode, errorMessage, nil)
return
}
result, callErr := handler.call(request.Context(), envelope.Method, envelope.Params)
@@ -87,6 +76,29 @@ type jsonRPCRequest struct {
Params json.RawMessage `json:"params"`
}
// decodeJSONRPCRequest keeps the hostile JSON boundary independently bounded
// and fuzzable. The returned code/message are the externally stable JSON-RPC
// parse or invalid-request result; callers must not inspect partial fields.
func decodeJSONRPCRequest(body []byte, maxBody int64) (jsonRPCRequest, int, string) {
if maxBody <= 0 {
maxBody = defaultJSONRPCBody
}
if int64(len(body)) > maxBody {
return jsonRPCRequest{}, jsonRPCInvalid, "request body exceeds limit"
}
var envelope jsonRPCRequest
decoder := json.NewDecoder(bytes.NewReader(body))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&envelope); err != nil {
return jsonRPCRequest{}, jsonRPCParseError, "invalid JSON"
}
var trailing any
if err := decoder.Decode(&trailing); err != io.EOF || envelope.JSONRPC != jsonRPCVersion || envelope.Method == "" || len(envelope.ID) == 0 || bytes.Equal(bytes.TrimSpace(envelope.ID), []byte("null")) {
return jsonRPCRequest{}, jsonRPCInvalid, "invalid JSON-RPC request"
}
return envelope, 0, ""
}
type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
+15
View File
@@ -95,3 +95,18 @@ func TestJSONRPCRejectsOversizeAndMalformedRequests_BH_CTL_16(t *testing.T) {
t.Fatalf("malformed response = %s", malformedBody)
}
}
func FuzzDecodeJSONRPCRequestBounded_SEC_CTL_01(f *testing.F) {
f.Add([]byte(`{"jsonrpc":"2.0","id":1,"method":"listClients","params":{}}`))
f.Add([]byte(`{"jsonrpc":"2.0","id":null,"method":"listClients"}`))
f.Add([]byte(`{"jsonrpc":"2.0","id":1,"method":"listClients"}{}`))
f.Fuzz(func(t *testing.T, body []byte) {
// Keep fuzzing at the same independently enforced boundary as the
// production handler rather than allowing a corpus entry to allocate
// unbounded JSON decoder state.
if len(body) > 64<<10 {
return
}
_, _, _ = decodeJSONRPCRequest(body, 64<<10)
})
}