Skip to content

Commit

Permalink
Adds some size limiting features to transactions to help prevent abuse.
Browse files Browse the repository at this point in the history
  • Loading branch information
James Phillips committed May 13, 2016
1 parent fbfb90a commit 570d46a
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 3 deletions.
28 changes: 28 additions & 0 deletions command/agent/txn_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -90,18 +97,30 @@ 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 {
resp.WriteHeader(http.StatusRequestEntityTooLarge)
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)
Expand All @@ -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
}

Expand Down
78 changes: 75 additions & 3 deletions command/agent/txn_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
[
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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": {
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions website/source/docs/agent/http/kv.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 570d46a

Please sign in to comment.