Add input validation for auto-config JWT authorization checks.
This commit is contained in:
parent
6196be1f98
commit
cfcd9f2a2c
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:security
|
||||||
|
auto-config: Added input validation for auto-config JWT authorization checks. Prior to this change, it was possible for malicious actors to construct requests which incorrectly pass custom JWT claim validation for the `AutoConfig.InitialConfiguration` endpoint. Now, only a subset of characters are allowed for the input before evaluating the bexpr.
|
||||||
|
```
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
|
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/connect"
|
"github.com/hashicorp/consul/agent/connect"
|
||||||
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
|
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
|
||||||
|
"github.com/hashicorp/consul/agent/dns"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib/template"
|
"github.com/hashicorp/consul/lib/template"
|
||||||
"github.com/hashicorp/consul/proto/pbautoconf"
|
"github.com/hashicorp/consul/proto/pbautoconf"
|
||||||
|
@ -51,6 +53,11 @@ type jwtAuthorizer struct {
|
||||||
claimAssertions []string
|
claimAssertions []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate any quote or whitespace characters that could cause an escape with bexpr.
|
||||||
|
// This includes an extra single-quote character not specified in the grammar for safety in case it is later added.
|
||||||
|
// https://github.com/hashicorp/go-bexpr/blob/v0.1.11/grammar/grammar.peg#L188-L191
|
||||||
|
var invalidSegmentName = regexp.MustCompile("[`'\"\\s]+")
|
||||||
|
|
||||||
func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfigOptions, error) {
|
func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfigOptions, error) {
|
||||||
// perform basic JWT Authorization
|
// perform basic JWT Authorization
|
||||||
identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
|
identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
|
||||||
|
@ -59,6 +66,21 @@ func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfig
|
||||||
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
|
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure provided data cannot escape the RHS of a bexpr for security.
|
||||||
|
// This is not the cleanest way to prevent this behavior. Ideally, the bexpr would allow us to
|
||||||
|
// inject a variable on the RHS for comparison as well, but it would be a complex change to implement
|
||||||
|
// that would likely break backwards-compatibility in certain circumstances.
|
||||||
|
if dns.InvalidNameRe.MatchString(req.Node) {
|
||||||
|
return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "node", req.Node)
|
||||||
|
}
|
||||||
|
if invalidSegmentName.MatchString(req.Segment) {
|
||||||
|
return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "segment", req.Segment)
|
||||||
|
}
|
||||||
|
if req.Partition != "" && !dns.IsValidLabel(req.Partition) {
|
||||||
|
return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "partition", req.Partition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that every value in this mapping is safe to interpolate before using it.
|
||||||
varMap := map[string]string{
|
varMap := map[string]string{
|
||||||
"node": req.Node,
|
"node": req.Node,
|
||||||
"segment": req.Segment,
|
"segment": req.Segment,
|
||||||
|
|
|
@ -92,9 +92,9 @@ func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{})
|
||||||
// TestAutoConfigInitialConfiguration is really an integration test of all the moving parts of the AutoConfig.InitialConfiguration RPC.
|
// TestAutoConfigInitialConfiguration is really an integration test of all the moving parts of the AutoConfig.InitialConfiguration RPC.
|
||||||
// Full testing of the individual parts will not be done in this test:
|
// Full testing of the individual parts will not be done in this test:
|
||||||
//
|
//
|
||||||
// * Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer)
|
// - Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer)
|
||||||
// * Each of the individual config generation functions. These can be unit tested separately and should NOT
|
// - Each of the individual config generation functions. These can be unit tested separately and should NOT
|
||||||
// require running test servers
|
// require running test servers
|
||||||
func TestAutoConfigInitialConfiguration(t *testing.T) {
|
func TestAutoConfigInitialConfiguration(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("too slow for testing.Short")
|
t.Skip("too slow for testing.Short")
|
||||||
|
@ -236,6 +236,29 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
|
||||||
},
|
},
|
||||||
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
|
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
|
||||||
},
|
},
|
||||||
|
"bad-req-node": {
|
||||||
|
request: &pbautoconf.AutoConfigRequest{
|
||||||
|
Node: "bad node",
|
||||||
|
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||||
|
},
|
||||||
|
err: "Invalid request field. node =",
|
||||||
|
},
|
||||||
|
"bad-req-segment": {
|
||||||
|
request: &pbautoconf.AutoConfigRequest{
|
||||||
|
Node: "test-node",
|
||||||
|
Segment: "bad segment",
|
||||||
|
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||||
|
},
|
||||||
|
err: "Invalid request field. segment =",
|
||||||
|
},
|
||||||
|
"bad-req-partition": {
|
||||||
|
request: &pbautoconf.AutoConfigRequest{
|
||||||
|
Node: "test-node",
|
||||||
|
Partition: "bad partition",
|
||||||
|
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
|
||||||
|
},
|
||||||
|
err: "Invalid request field. partition =",
|
||||||
|
},
|
||||||
"claim-assertion-failed": {
|
"claim-assertion-failed": {
|
||||||
request: &pbautoconf.AutoConfigRequest{
|
request: &pbautoconf.AutoConfigRequest{
|
||||||
Node: "test-node",
|
Node: "test-node",
|
||||||
|
@ -850,3 +873,39 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
|
||||||
|
|
||||||
backend.AssertExpectations(t)
|
backend.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoConfig_invalidSegmentName(t *testing.T) {
|
||||||
|
invalid := []string{
|
||||||
|
"\n",
|
||||||
|
"\r",
|
||||||
|
"\t",
|
||||||
|
"`",
|
||||||
|
`'`,
|
||||||
|
`"`,
|
||||||
|
` `,
|
||||||
|
`a b`,
|
||||||
|
`a'b`,
|
||||||
|
`a or b`,
|
||||||
|
`a and b`,
|
||||||
|
`segment name`,
|
||||||
|
`segment"name`,
|
||||||
|
`"segment"name`,
|
||||||
|
`"segment" name`,
|
||||||
|
`segment'name'`,
|
||||||
|
}
|
||||||
|
valid := []string{
|
||||||
|
``,
|
||||||
|
`a`,
|
||||||
|
`a.b`,
|
||||||
|
`a.b.c`,
|
||||||
|
`a-b-c`,
|
||||||
|
`segment.name`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range invalid {
|
||||||
|
require.True(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s)
|
||||||
|
}
|
||||||
|
for _, s := range valid {
|
||||||
|
require.False(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue