Skip to content

Commit 77cda12

Browse files
vault: harden auth review findings
- Cap JWKS response body to 1 MB to prevent resource exhaustion - Make all AuthResult fields unexported with accessor methods - Add iat claim validation to JWT parser - Annotate unreachable return in allowlist retry loop - Fix misleading test name for digest verification delegation
1 parent 8748ffd commit 77cda12

5 files changed

Lines changed: 39 additions & 20 deletions

File tree

core/capabilities/vault/allow_list_based_auth.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ func (r *allowListBasedAuth) AuthorizeRequest(ctx context.Context, req jsonrpc.R
7171

7272
digestKey := string(allowlistedRequest.RequestDigest[:])
7373
r.lggr.Debugw("AllowListBasedAuth authorization succeeded", "method", req.Method, "requestID", req.ID, "authorizedRequestStr", digestKey, "owner", allowlistedRequest.Owner.Hex(), "expiryTimestamp", allowlistedRequest.ExpiryTimestamp)
74-
return &AuthResult{
75-
workflowOwner: allowlistedRequest.Owner.Hex(),
76-
Digest: digestKey,
77-
ExpiresAt: int64(allowlistedRequest.ExpiryTimestamp),
78-
}, nil
74+
return NewAllowListBasedAuthResult(
75+
allowlistedRequest.Owner.Hex(),
76+
digestKey,
77+
int64(allowlistedRequest.ExpiryTimestamp),
78+
), nil
7979
}
8080

8181
func (r *allowListBasedAuth) findAllowlistedItemWithRetry(ctx context.Context, req jsonrpc.Request[json.RawMessage], requestDigest string, requestDigestBytes32 [32]byte) (*workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest, []string, error) {
@@ -109,7 +109,7 @@ func (r *allowListBasedAuth) findAllowlistedItemWithRetry(ctx context.Context, r
109109
}
110110
}
111111

112-
return nil, nil, nil
112+
return nil, nil, nil // unreachable: loop always returns
113113
}
114114

115115
func (r *allowListBasedAuth) fetchAllowlistedItem(allowListedRequests []workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest, digest [32]byte) *workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest {

core/capabilities/vault/allow_list_based_auth_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ func testAuthForRequests(t *testing.T, allowlistedRequest, notAllowlistedRequest
179179
authResult, err := auth.AuthorizeRequest(t.Context(), allowlistedRequest)
180180
require.NoError(t, err)
181181
require.Equal(t, owner.Hex(), authResult.AuthorizedOwner())
182-
require.Equal(t, expiry, authResult.ExpiresAt)
183-
require.NotEmpty(t, authResult.Digest)
182+
require.Equal(t, expiry, authResult.GetExpiresAt())
183+
require.NotEmpty(t, authResult.GetDigest())
184184

185185
// Same request is still authorized here; replay protection lives in the generic Authorizer.
186186
authResult, err = auth.AuthorizeRequest(t.Context(), allowlistedRequest)
@@ -245,5 +245,5 @@ func TestAllowListBasedAuth_RetriesUntilRequestIsAllowlisted(t *testing.T) {
245245
authResult, err := auth.AuthorizeRequest(t.Context(), req)
246246
require.NoError(t, err)
247247
require.Equal(t, owner.Hex(), authResult.AuthorizedOwner())
248-
require.Equal(t, expiry, authResult.ExpiresAt)
248+
require.Equal(t, expiry, authResult.GetExpiresAt())
249249
}

core/capabilities/vault/authorizer.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,24 @@ import (
1414
type AuthResult struct {
1515
orgID string
1616
workflowOwner string
17-
Digest string
18-
ExpiresAt int64
17+
digest string
18+
expiresAt int64
1919
}
2020

2121
func NewAllowListBasedAuthResult(workflowOwner, digest string, expiresAt int64) *AuthResult {
2222
return &AuthResult{
2323
workflowOwner: workflowOwner,
24-
Digest: digest,
25-
ExpiresAt: expiresAt,
24+
digest: digest,
25+
expiresAt: expiresAt,
2626
}
2727
}
2828

2929
func NewJWTBasedAuthResult(orgID, workflowOwner, digest string, expiresAt int64) *AuthResult {
3030
return &AuthResult{
3131
orgID: orgID,
3232
workflowOwner: workflowOwner,
33-
Digest: digest,
34-
ExpiresAt: expiresAt,
33+
digest: digest,
34+
expiresAt: expiresAt,
3535
}
3636
}
3737

@@ -46,6 +46,23 @@ func (a *AuthResult) AuthorizedOwner() string {
4646
return a.workflowOwner
4747
}
4848

49+
// GetDigest returns the request digest used for replay protection.
50+
func (a *AuthResult) GetDigest() string {
51+
if a == nil {
52+
return ""
53+
}
54+
return a.digest
55+
}
56+
57+
// GetExpiresAt returns the unix timestamp (UTC) after which this
58+
// authorization is no longer valid.
59+
func (a *AuthResult) GetExpiresAt() int64 {
60+
if a == nil {
61+
return 0
62+
}
63+
return a.expiresAt
64+
}
65+
4966
// GetUntrustedWorkflowOwner returns the workflow owner only for JWTBasedAuth results.
5067
func (a *AuthResult) GetUntrustedWorkflowOwner() (string, error) {
5168
if a == nil {
@@ -93,11 +110,11 @@ func (a *authorizer) AuthorizeRequest(ctx context.Context, req jsonrpc.Request[j
93110
a.lggr.Errorw("auth mechanism returned nil auth result", "method", req.Method, "requestID", req.ID, "hasAuth", req.Auth != "")
94111
return nil, err
95112
}
96-
if err := a.replayGuard.CheckAndRecord(authResult.Digest, authResult.ExpiresAt); err != nil {
97-
a.lggr.Debugw("replay guard rejected request", "method", req.Method, "requestID", req.ID, "owner", authResult.AuthorizedOwner(), "digest", authResult.Digest, "expiresAt", authResult.ExpiresAt, "hasAuth", req.Auth != "", "error", err)
113+
if err := a.replayGuard.CheckAndRecord(authResult.GetDigest(), authResult.GetExpiresAt()); err != nil {
114+
a.lggr.Debugw("replay guard rejected request", "method", req.Method, "requestID", req.ID, "owner", authResult.AuthorizedOwner(), "digest", authResult.GetDigest(), "expiresAt", authResult.GetExpiresAt(), "hasAuth", req.Auth != "", "error", err)
98115
return nil, err
99116
}
100-
a.lggr.Debugw("request authorized", "method", req.Method, "requestID", req.ID, "owner", authResult.AuthorizedOwner(), "digest", authResult.Digest, "expiresAt", authResult.ExpiresAt, "hasAuth", req.Auth != "")
117+
a.lggr.Debugw("request authorized", "method", req.Method, "requestID", req.ID, "owner", authResult.AuthorizedOwner(), "digest", authResult.GetDigest(), "expiresAt", authResult.GetExpiresAt(), "hasAuth", req.Auth != "")
101118
return authResult, nil
102119
}
103120

core/capabilities/vault/authorizer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func TestAuthorizer_UsesJWTWhenGateEnabled(t *testing.T) {
8686
require.Equal(t, "0xworkflow", untrustedWorkflowOwner)
8787
}
8888

89-
func TestAuthorizer_RejectsJWTDigestMismatch(t *testing.T) {
89+
func TestAuthorizer_DelegatesDigestVerificationToJWTAuth(t *testing.T) {
9090
params, err := json.Marshal(vaultcommon.CreateSecretsRequest{})
9191
require.NoError(t, err)
9292

core/capabilities/vault/jwt_based_auth.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ func (v *jwtBasedAuth) validateToken(ctx context.Context, tokenString string) (*
200200
jwt.WithIssuer(v.issuerURL),
201201
jwt.WithAudience(v.audience),
202202
jwt.WithExpirationRequired(),
203+
jwt.WithIssuedAt(),
203204
)
204205
if err != nil {
205206
return nil, fmt.Errorf("%w: %w", ErrInvalidToken, err)
@@ -346,7 +347,8 @@ func (v *jwtBasedAuth) refreshJWKS(ctx context.Context) error {
346347
return fmt.Errorf("%w: HTTP %d", ErrJWKSFetchFailed, resp.StatusCode)
347348
}
348349

349-
body, err := io.ReadAll(resp.Body)
350+
const maxJWKSBodySize = 1 << 20 // 1 MB
351+
body, err := io.ReadAll(io.LimitReader(resp.Body, maxJWKSBodySize))
350352
if err != nil {
351353
return fmt.Errorf("%w: %w", ErrJWKSFetchFailed, err)
352354
}

0 commit comments

Comments
 (0)