Adds some size limiting features to transactions to help prevent abuse.
This commit is contained in:
parent
5fd99b13ef
commit
77ae55c692
|
@ -10,6 +10,13 @@ import (
|
||||||
"github.com/hashicorp/consul/consul/structs"
|
"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.
|
// decodeValue decodes the value member of the given operation.
|
||||||
func decodeValue(rawKV interface{}) error {
|
func decodeValue(rawKV interface{}) error {
|
||||||
rawMap, ok := rawKV.(map[string]interface{})
|
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
|
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
|
// Convert the KV API format into the RPC format. Note that fixupKVOps
|
||||||
// above will have already converted the base64 encoded strings into
|
// above will have already converted the base64 encoded strings into
|
||||||
// byte arrays so we can assign right over.
|
// byte arrays so we can assign right over.
|
||||||
var opsRPC structs.TxnOps
|
var opsRPC structs.TxnOps
|
||||||
var writes int
|
var writes int
|
||||||
|
var netKVSize int
|
||||||
for _, in := range ops {
|
for _, in := range ops {
|
||||||
if in.KV != nil {
|
if in.KV != nil {
|
||||||
if size := len(in.KV.Value); size > maxKVSize {
|
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)",
|
resp.Write([]byte(fmt.Sprintf("Value for key %q is too large (%d > %d bytes)",
|
||||||
in.KV.Key, size, maxKVSize)))
|
in.KV.Key, size, maxKVSize)))
|
||||||
return nil, 0, false
|
return nil, 0, false
|
||||||
|
} else {
|
||||||
|
netKVSize += size
|
||||||
}
|
}
|
||||||
|
|
||||||
verb := structs.KVSOp(in.KV.Verb)
|
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)
|
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
|
return opsRPC, writes, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
httpTest(t, func(srv *HTTPServer) {
|
||||||
buf := bytes.NewBuffer([]byte(fmt.Sprintf(`
|
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) {
|
func TestTxnEndpoint_KV_Actions(t *testing.T) {
|
||||||
httpTest(t, func(srv *HTTPServer) {
|
httpTest(t, func(srv *HTTPServer) {
|
||||||
// Make sure all incoming fields get converted properly to the internal
|
// 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
|
// Do a read-only transaction that should get routed to the
|
||||||
// fast-path endpoint.
|
// fast-path endpoint.
|
||||||
{
|
{
|
||||||
buf := bytes.NewBuffer([]byte(fmt.Sprintf(`
|
buf := bytes.NewBuffer([]byte(`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"KV": {
|
"KV": {
|
||||||
|
@ -174,7 +246,7 @@ func TestTxnEndpoint_KV_Actions(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
`, index)))
|
`))
|
||||||
req, err := http.NewRequest("PUT", "/v1/txn", buf)
|
req, err := http.NewRequest("PUT", "/v1/txn", buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
|
|
|
@ -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
|
`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
|
in future versions of Consul to be mixed with key/value operations. The following fields
|
||||||
are available:
|
are available:
|
||||||
|
|
Loading…
Reference in New Issue