diff --git a/command/agent/txn_endpoint.go b/command/agent/txn_endpoint.go index 00d511062..17d6b6ea4 100644 --- a/command/agent/txn_endpoint.go +++ b/command/agent/txn_endpoint.go @@ -10,6 +10,13 @@ import ( "github.com/hashicorp/consul/consul/structs" ) +const ( + // maxTxnOps is used to set an upper limit on the number of operations + // inside a transaction. If there are more operations than this, then the + // client is likely abusing transactions. + maxTxnOps = 500 +) + // decodeValue decodes the value member of the given operation. func decodeValue(rawKV interface{}) error { rawMap, ok := rawKV.(map[string]interface{}) @@ -90,11 +97,21 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st return nil, 0, false } + // Enforce a reasonable upper limit on the number of operations in a + // transaction in order to curb abuse. + if size := len(ops); size > maxTxnOps { + resp.WriteHeader(http.StatusRequestEntityTooLarge) + resp.Write([]byte(fmt.Sprintf("Transaction contains too many operations (%d > %d)", + size, maxTxnOps))) + return nil, 0, false + } + // Convert the KV API format into the RPC format. Note that fixupKVOps // above will have already converted the base64 encoded strings into // byte arrays so we can assign right over. var opsRPC structs.TxnOps var writes int + var netKVSize int for _, in := range ops { if in.KV != nil { if size := len(in.KV.Value); size > maxKVSize { @@ -102,6 +119,8 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st resp.Write([]byte(fmt.Sprintf("Value for key %q is too large (%d > %d bytes)", in.KV.Key, size, maxKVSize))) return nil, 0, false + } else { + netKVSize += size } verb := structs.KVSOp(in.KV.Verb) @@ -126,6 +145,15 @@ func (s *HTTPServer) convertOps(resp http.ResponseWriter, req *http.Request) (st opsRPC = append(opsRPC, out) } } + + // Enforce an overall size limit to help prevent abuse. + if netKVSize > maxKVSize { + resp.WriteHeader(http.StatusRequestEntityTooLarge) + resp.Write([]byte(fmt.Sprintf("Cumulative size of key data is too large (%d > %d bytes)", + netKVSize, maxKVSize))) + return nil, 0, false + } + return opsRPC, writes, true } diff --git a/command/agent/txn_endpoint_test.go b/command/agent/txn_endpoint_test.go index 7f035c108..95de2e726 100644 --- a/command/agent/txn_endpoint_test.go +++ b/command/agent/txn_endpoint_test.go @@ -51,7 +51,7 @@ func TestTxnEndpoint_Bad_Method(t *testing.T) { }) } -func TestTxnEndpoint_Bad_Size(t *testing.T) { +func TestTxnEndpoint_Bad_Size_Item(t *testing.T) { httpTest(t, func(srv *HTTPServer) { buf := bytes.NewBuffer([]byte(fmt.Sprintf(` [ @@ -79,6 +79,78 @@ func TestTxnEndpoint_Bad_Size(t *testing.T) { }) } +func TestTxnEndpoint_Bad_Size_Net(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + value := strings.Repeat("X", maxKVSize/2) + buf := bytes.NewBuffer([]byte(fmt.Sprintf(` +[ + { + "KV": { + "Verb": "set", + "Key": "key1", + "Value": %q + } + }, + { + "KV": { + "Verb": "set", + "Key": "key1", + "Value": %q + } + }, + { + "KV": { + "Verb": "set", + "Key": "key1", + "Value": %q + } + } +] +`, value, value, value))) + req, err := http.NewRequest("PUT", "/v1/txn", buf) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + if _, err := srv.Txn(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 413 { + t.Fatalf("expected 413, got %d", resp.Code) + } + }) +} + +func TestTxnEndpoint_Bad_Size_Ops(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + buf := bytes.NewBuffer([]byte(fmt.Sprintf(` +[ + %s + { + "KV": { + "Verb": "set", + "Key": "key", + "Value": "" + } + } +] +`, strings.Repeat(`{ "KV": { "Verb": "get", "Key": "key" } },`, 2*maxTxnOps)))) + req, err := http.NewRequest("PUT", "/v1/txn", buf) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + if _, err := srv.Txn(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if resp.Code != 413 { + t.Fatalf("expected 413, got %d", resp.Code) + } + }) +} + func TestTxnEndpoint_KV_Actions(t *testing.T) { httpTest(t, func(srv *HTTPServer) { // Make sure all incoming fields get converted properly to the internal @@ -165,7 +237,7 @@ func TestTxnEndpoint_KV_Actions(t *testing.T) { // Do a read-only transaction that should get routed to the // fast-path endpoint. { - buf := bytes.NewBuffer([]byte(fmt.Sprintf(` + buf := bytes.NewBuffer([]byte(` [ { "KV": { @@ -174,7 +246,7 @@ func TestTxnEndpoint_KV_Actions(t *testing.T) { } } ] -`, index))) +`)) req, err := http.NewRequest("PUT", "/v1/txn", buf) if err != nil { t.Fatalf("err: %v", err) diff --git a/website/source/docs/agent/http/kv.html.markdown b/website/source/docs/agent/http/kv.html.markdown index 9b4113497..b65d3a6a5 100644 --- a/website/source/docs/agent/http/kv.html.markdown +++ b/website/source/docs/agent/http/kv.html.markdown @@ -200,6 +200,8 @@ transaction, which looks like this: ] ``` +Up to 500 operations may be present in a single transaction. + `KV` is the only available operation type, though other types of operations may be added in future versions of Consul to be mixed with key/value operations. The following fields are available: