Skip to content

Commit 02ebc1d

Browse files
backport of commit 2c88125 (#14582)
This pull request was automerged via backport-assistant
1 parent d839dec commit 02ebc1d

File tree

3 files changed

+87
-3
lines changed

3 files changed

+87
-3
lines changed

.changelog/14577.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:security
2+
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.
3+
```

agent/consul/auto_config_endpoint.go

+22
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import (
55
"crypto/x509"
66
"encoding/base64"
77
"fmt"
8+
"regexp"
89

910
"github.com/hashicorp/consul/acl"
1011

1112
bexpr "github.com/hashicorp/go-bexpr"
1213

1314
"github.com/hashicorp/consul/agent/connect"
1415
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
16+
"github.com/hashicorp/consul/agent/dns"
1517
"github.com/hashicorp/consul/agent/structs"
1618
"github.com/hashicorp/consul/lib/template"
1719
"github.com/hashicorp/consul/proto/pbautoconf"
@@ -51,6 +53,11 @@ type jwtAuthorizer struct {
5153
claimAssertions []string
5254
}
5355

56+
// Invalidate any quote or whitespace characters that could cause an escape with bexpr.
57+
// This includes an extra single-quote character not specified in the grammar for safety in case it is later added.
58+
// https://github.com/hashicorp/go-bexpr/blob/v0.1.11/grammar/grammar.peg#L188-L191
59+
var invalidSegmentName = regexp.MustCompile("[`'\"\\s]+")
60+
5461
func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfigOptions, error) {
5562
// perform basic JWT Authorization
5663
identity, err := a.validator.ValidateLogin(context.Background(), req.JWT)
@@ -59,6 +66,21 @@ func (a *jwtAuthorizer) Authorize(req *pbautoconf.AutoConfigRequest) (AutoConfig
5966
return AutoConfigOptions{}, acl.PermissionDenied("Failed JWT authorization: %v", err)
6067
}
6168

69+
// Ensure provided data cannot escape the RHS of a bexpr for security.
70+
// This is not the cleanest way to prevent this behavior. Ideally, the bexpr would allow us to
71+
// inject a variable on the RHS for comparison as well, but it would be a complex change to implement
72+
// that would likely break backwards-compatibility in certain circumstances.
73+
if dns.InvalidNameRe.MatchString(req.Node) {
74+
return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "node", req.Node)
75+
}
76+
if invalidSegmentName.MatchString(req.Segment) {
77+
return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "segment", req.Segment)
78+
}
79+
if req.Partition != "" && !dns.IsValidLabel(req.Partition) {
80+
return AutoConfigOptions{}, fmt.Errorf("Invalid request field. %v = `%v`", "partition", req.Partition)
81+
}
82+
83+
// Ensure that every value in this mapping is safe to interpolate before using it.
6284
varMap := map[string]string{
6385
"node": req.Node,
6486
"segment": req.Segment,

agent/consul/auto_config_endpoint_test.go

+62-3
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ func signJWTWithStandardClaims(t *testing.T, privKey string, claims interface{})
9292
// TestAutoConfigInitialConfiguration is really an integration test of all the moving parts of the AutoConfig.InitialConfiguration RPC.
9393
// Full testing of the individual parts will not be done in this test:
9494
//
95-
// * Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer)
96-
// * Each of the individual config generation functions. These can be unit tested separately and should NOT
97-
// require running test servers
95+
// - Any implementations of the AutoConfigAuthorizer interface (although these test do use the jwtAuthorizer)
96+
// - Each of the individual config generation functions. These can be unit tested separately and should NOT
97+
// require running test servers
9898
func TestAutoConfigInitialConfiguration(t *testing.T) {
9999
if testing.Short() {
100100
t.Skip("too slow for testing.Short")
@@ -236,6 +236,29 @@ func TestAutoConfigInitialConfiguration(t *testing.T) {
236236
},
237237
err: "Permission denied: Failed JWT authorization: no known key successfully validated the token signature",
238238
},
239+
"bad-req-node": {
240+
request: &pbautoconf.AutoConfigRequest{
241+
Node: "bad node",
242+
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
243+
},
244+
err: "Invalid request field. node =",
245+
},
246+
"bad-req-segment": {
247+
request: &pbautoconf.AutoConfigRequest{
248+
Node: "test-node",
249+
Segment: "bad segment",
250+
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
251+
},
252+
err: "Invalid request field. segment =",
253+
},
254+
"bad-req-partition": {
255+
request: &pbautoconf.AutoConfigRequest{
256+
Node: "test-node",
257+
Partition: "bad partition",
258+
JWT: signJWTWithStandardClaims(t, priv, map[string]interface{}{"consul_node_name": "test-node"}),
259+
},
260+
err: "Invalid request field. partition =",
261+
},
239262
"claim-assertion-failed": {
240263
request: &pbautoconf.AutoConfigRequest{
241264
Node: "test-node",
@@ -850,3 +873,39 @@ func TestAutoConfig_updateJoinAddressesInConfig(t *testing.T) {
850873

851874
backend.AssertExpectations(t)
852875
}
876+
877+
func TestAutoConfig_invalidSegmentName(t *testing.T) {
878+
invalid := []string{
879+
"\n",
880+
"\r",
881+
"\t",
882+
"`",
883+
`'`,
884+
`"`,
885+
` `,
886+
`a b`,
887+
`a'b`,
888+
`a or b`,
889+
`a and b`,
890+
`segment name`,
891+
`segment"name`,
892+
`"segment"name`,
893+
`"segment" name`,
894+
`segment'name'`,
895+
}
896+
valid := []string{
897+
``,
898+
`a`,
899+
`a.b`,
900+
`a.b.c`,
901+
`a-b-c`,
902+
`segment.name`,
903+
}
904+
905+
for _, s := range invalid {
906+
require.True(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s)
907+
}
908+
for _, s := range valid {
909+
require.False(t, invalidSegmentName.MatchString(s), "incorrect match: %v", s)
910+
}
911+
}

0 commit comments

Comments
 (0)