Skip to content

Commit 678a897

Browse files
security: enforce strict input validation and payload bounds in tokens.Service
This commit implements a comprehensive security validation layer to mitigate resource exhaustion and DoS risks. - Refactored Validator layer to serve as the primary enforcement point for payload and action constraints before transaction commitment. - Updated ValidationConfig to incorporate configurable limits for total token request size and action counts. - Added strict protobuf unmarshaling bounds in driver/request.go for all message types. - Propagated configurable validation limits through TMS and service layers for consistent enforcement. - Updated validator mocks and test suites to verify new security bounds. Fixes #1608
1 parent f96ff8b commit 678a897

11 files changed

Lines changed: 504 additions & 5 deletions

File tree

token/config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,24 @@ func (m *Configuration) IsSet(key string) bool {
2929
func (m *Configuration) UnmarshalKey(key string, rawVal interface{}) error {
3030
return m.cm.UnmarshalKey(key, rawVal)
3131
}
32+
33+
// GetValidationConfig returns the validation configuration
34+
func (m *Configuration) GetValidationConfig() (driver.ValidationConfig, error) {
35+
config := driver.ValidationConfig{
36+
MaxTokenPayloadSize: 2 * 1024 * 1024,
37+
MaxTokenOutputsPerTx: 1000,
38+
MaxBulkDeleteSize: 10000,
39+
MaxWalletIDSize: 1024,
40+
MaxOwnerRawSize: 16 * 1024,
41+
MaxIssuerRawSize: 16 * 1024,
42+
MaxTokenRequestSize: 2 * 1024 * 1024,
43+
MaxActionCount: 1000,
44+
}
45+
if m.cm.IsSet("validation") {
46+
if err := m.cm.UnmarshalKey("validation", &config); err != nil {
47+
return config, err
48+
}
49+
}
50+
51+
return config, nil
52+
}

token/core/common/validator.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ type Validator[P driver.PublicParameters, T driver.Input, TA driver.TransferActi
7272
// If set to a specific version (e.g., driver.ProtocolV2), only requests with that version
7373
// or higher will be accepted, rejecting older protocol versions.
7474
MinProtocolVersion uint32
75+
76+
// ValidationConfig specifies the resource limits for token requests.
77+
ValidationConfig driver.ValidationConfig
7578
}
7679

7780
// NewValidator returns a new Validator instance for the passed arguments.
@@ -92,6 +95,16 @@ func NewValidator[P driver.PublicParameters, T driver.Input, TA driver.TransferA
9295
TransferValidators: transferValidators,
9396
IssueValidators: issueValidators,
9497
AuditingValidators: auditingValidators,
98+
ValidationConfig: driver.ValidationConfig{
99+
MaxTokenPayloadSize: 2 * 1024 * 1024,
100+
MaxTokenOutputsPerTx: 1000,
101+
MaxBulkDeleteSize: 10000,
102+
MaxWalletIDSize: 1024,
103+
MaxOwnerRawSize: 16 * 1024,
104+
MaxIssuerRawSize: 16 * 1024,
105+
MaxTokenRequestSize: 2 * 1024 * 1024,
106+
MaxActionCount: 1000,
107+
},
95108
}
96109
}
97110

@@ -102,18 +115,30 @@ func (v *Validator[P, T, TA, IA, DS]) SetMinProtocolVersion(version uint32) {
102115
v.MinProtocolVersion = version
103116
}
104117

118+
// SetValidationConfig configures the validation limits for the validator.
119+
func (v *Validator[P, T, TA, IA, DS]) SetValidationConfig(config driver.ValidationConfig) {
120+
v.ValidationConfig = config
121+
}
122+
105123
// VerifyTokenRequestFromRaw verifies a token request from its raw representation.
106124
func (v *Validator[P, T, TA, IA, DS]) VerifyTokenRequestFromRaw(ctx context.Context, getState driver.GetStateFnc, anchor driver.TokenRequestAnchor, raw []byte) ([]interface{}, driver.ValidationAttributes, error) {
107125
logger.DebugfContext(ctx, "Verify token request from raw")
108126
if len(raw) == 0 {
109127
return nil, nil, errors.New("empty token request")
110128
}
129+
if v.ValidationConfig.MaxTokenRequestSize > 0 && len(raw) > v.ValidationConfig.MaxTokenRequestSize {
130+
return nil, nil, errors.Errorf("token request too large: %d > %d", len(raw), v.ValidationConfig.MaxTokenRequestSize)
131+
}
111132
tr := &driver.TokenRequest{}
112133
err := tr.FromBytes(raw)
113134
if err != nil {
114135
return nil, nil, errors.Wrap(err, "failed to unmarshal token request")
115136
}
116137

138+
if v.ValidationConfig.MaxActionCount > 0 && len(tr.Issues)+len(tr.Transfers) > v.ValidationConfig.MaxActionCount {
139+
return nil, nil, errors.Errorf("too many actions: %d > %d", len(tr.Issues)+len(tr.Transfers), v.ValidationConfig.MaxActionCount)
140+
}
141+
117142
// Validate protocol version
118143
if tr.Version == 0 {
119144
return nil, nil, driver.ErrInvalidVersion
@@ -172,6 +197,18 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTokenRequest(
172197
if err != nil {
173198
return nil, nil, errors.Wrapf(err, "failed to verify issue actions [%s]", anchor)
174199
}
200+
201+
totalOutputs := 0
202+
for _, action := range ia {
203+
totalOutputs += action.NumOutputs()
204+
}
205+
for _, action := range ta {
206+
totalOutputs += action.NumOutputs()
207+
}
208+
if v.ValidationConfig.MaxTokenOutputsPerTx > 0 && totalOutputs > v.ValidationConfig.MaxTokenOutputsPerTx {
209+
return nil, nil, errors.Errorf("too many token outputs: %d > %d", totalOutputs, v.ValidationConfig.MaxTokenOutputsPerTx)
210+
}
211+
175212
err = v.verifyTransfers(ctx, anchor, tr, ledger, ta, signatureProvider, attributes)
176213
if err != nil {
177214
return nil, nil, errors.Wrapf(err, "failed to verify transfer actions [%s]", anchor)
@@ -251,6 +288,27 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyIssue(
251288
MetadataCounter: map[string]int{},
252289
Attributes: attributes,
253290
}
291+
292+
// Check outputs
293+
if v.ValidationConfig.MaxTokenPayloadSize > 0 || v.ValidationConfig.MaxOwnerRawSize > 0 {
294+
outputs := action.GetOutputs()
295+
for i, output := range outputs {
296+
raw, err := output.Serialize()
297+
if err != nil {
298+
return errors.Wrapf(err, "failed to serialize output at index %d", i)
299+
}
300+
if v.ValidationConfig.MaxTokenPayloadSize > 0 && len(raw) > v.ValidationConfig.MaxTokenPayloadSize {
301+
return errors.Errorf("output payload too large at index %d: %d > %d", i, len(raw), v.ValidationConfig.MaxTokenPayloadSize)
302+
}
303+
if v.ValidationConfig.MaxOwnerRawSize > 0 && len(output.GetOwner()) > v.ValidationConfig.MaxOwnerRawSize {
304+
return errors.Errorf("owner raw too large at index %d: %d > %d", i, len(output.GetOwner()), v.ValidationConfig.MaxOwnerRawSize)
305+
}
306+
}
307+
}
308+
if v.ValidationConfig.MaxIssuerRawSize > 0 && len(action.GetIssuer()) > v.ValidationConfig.MaxIssuerRawSize {
309+
return errors.Errorf("issuer raw too large: %d > %d", len(action.GetIssuer()), v.ValidationConfig.MaxIssuerRawSize)
310+
}
311+
254312
for _, v := range v.IssueValidators {
255313
if err := v(ctx, context); err != nil {
256314
return err
@@ -314,6 +372,27 @@ func (v *Validator[P, T, TA, IA, DS]) VerifyTransfer(
314372
MetadataCounter: map[MetadataCounterID]int{},
315373
Attributes: attributes,
316374
}
375+
376+
// Check outputs
377+
if v.ValidationConfig.MaxTokenPayloadSize > 0 || v.ValidationConfig.MaxOwnerRawSize > 0 {
378+
outputs := action.GetOutputs()
379+
for i, output := range outputs {
380+
raw, err := output.Serialize()
381+
if err != nil {
382+
return errors.Wrapf(err, "failed to serialize output at index %d", i)
383+
}
384+
if v.ValidationConfig.MaxTokenPayloadSize > 0 && len(raw) > v.ValidationConfig.MaxTokenPayloadSize {
385+
return errors.Errorf("output payload too large at index %d: %d > %d", i, len(raw), v.ValidationConfig.MaxTokenPayloadSize)
386+
}
387+
if v.ValidationConfig.MaxOwnerRawSize > 0 && len(output.GetOwner()) > v.ValidationConfig.MaxOwnerRawSize {
388+
return errors.Errorf("owner raw too large at index %d: %d > %d", i, len(output.GetOwner()), v.ValidationConfig.MaxOwnerRawSize)
389+
}
390+
}
391+
}
392+
if v.ValidationConfig.MaxIssuerRawSize > 0 && len(action.GetIssuer()) > v.ValidationConfig.MaxIssuerRawSize {
393+
return errors.Errorf("issuer raw too large: %d > %d", len(action.GetIssuer()), v.ValidationConfig.MaxIssuerRawSize)
394+
}
395+
317396
for _, v := range v.TransferValidators {
318397
if err := v(ctx, context); err != nil {
319398
return err

token/core/common/validator_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,32 @@ func TestValidatorWithCounterfeiter(t *testing.T) {
242242

243243
_, _, err = v.VerifyTokenRequestFromRaw(ctx, nil, anchor, []byte("invalid"))
244244
require.Error(t, err)
245+
246+
t.Run("MaxTokenRequestSize", func(t *testing.T) {
247+
vLimit := NewValidator[driver.PublicParameters, driver.Input, driver.TransferAction, driver.IssueAction, driver.Deserializer](
248+
logger, pp, des, ad, nil, nil, nil,
249+
)
250+
vLimit.ValidationConfig.MaxTokenRequestSize = 10
251+
_, _, err = vLimit.VerifyTokenRequestFromRaw(ctx, nil, anchor, make([]byte, 11))
252+
require.Error(t, err)
253+
assert.Contains(t, err.Error(), "token request too large")
254+
})
255+
256+
t.Run("MaxActionCount", func(t *testing.T) {
257+
vLimit := NewValidator[driver.PublicParameters, driver.Input, driver.TransferAction, driver.IssueAction, driver.Deserializer](
258+
logger, pp, des, ad, nil, nil, nil,
259+
)
260+
vLimit.ValidationConfig.MaxActionCount = 1
261+
trLarge := &driver.TokenRequest{
262+
Issues: [][]byte{[]byte("issue1")},
263+
Transfers: [][]byte{[]byte("transfer1")},
264+
Version: 1,
265+
}
266+
raw, _ := trLarge.Bytes()
267+
_, _, err = vLimit.VerifyTokenRequestFromRaw(ctx, nil, anchor, raw)
268+
require.Error(t, err)
269+
assert.Contains(t, err.Error(), "too many actions")
270+
})
245271
})
246272
}
247273

token/driver/mock/validator.go

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

token/driver/request.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ const (
2525
// MaxAnchorSize defines the maximum allowed size for anchor parameter in bytes.
2626
// This limit prevents potential DoS attacks through excessive memory allocation.
2727
MaxAnchorSize = 128 // bytes
28+
29+
// MaxTokenRequestSize defines the maximum allowed size for a serialized TokenRequest in bytes.
30+
// Default is 2MB to accommodate large Idemix transactions.
31+
MaxTokenRequestSize = 2 * 1024 * 1024
32+
33+
// MaxActionCount defines the maximum number of actions (issues + transfers) allowed in a single request.
34+
MaxActionCount = 1000
2835
)
2936

3037
// Typed errors for protocol validation
@@ -149,6 +156,16 @@ func (r *TokenRequest) FromProtos(tr *request.TokenRequest) error {
149156
// Store the version from the protobuf
150157
r.Version = tr.Version
151158

159+
if len(tr.Actions) > MaxActionCount {
160+
return errors.Errorf("too many actions: %d > %d", len(tr.Actions), MaxActionCount)
161+
}
162+
if len(tr.Signatures) > MaxActionCount {
163+
return errors.Errorf("too many signatures: %d > %d", len(tr.Signatures), MaxActionCount)
164+
}
165+
if tr.Auditing != nil && len(tr.Auditing.Signatures) > MaxActionCount {
166+
return errors.Errorf("too many auditor signatures: %d > %d", len(tr.Auditing.Signatures), MaxActionCount)
167+
}
168+
152169
for _, action := range tr.Actions {
153170
if action == nil {
154171
return errors.New("nil action found")
@@ -308,8 +325,17 @@ func (a *AuditableIdentity) ToProtos() (*request.AuditableIdentity, error) {
308325
}
309326

310327
func (a *AuditableIdentity) FromProtos(auditableIdentity *request.AuditableIdentity) error {
328+
if auditableIdentity == nil {
329+
return errors.New("auditable identity is nil")
330+
}
311331
a.Identity = ToIdentity(auditableIdentity.Identity)
332+
if len(a.Identity) > MaxOwnerRawSize {
333+
return errors.Errorf("identity too large: %d > %d", len(a.Identity), MaxOwnerRawSize)
334+
}
312335
a.AuditInfo = auditableIdentity.AuditInfo
336+
if len(a.AuditInfo) > MaxOwnerRawSize {
337+
return errors.Errorf("audit info too large: %d > %d", len(a.AuditInfo), MaxOwnerRawSize)
338+
}
313339

314340
return nil
315341
}
@@ -359,6 +385,9 @@ func (i *IssueOutputMetadata) FromProtos(outputsMetadata *request.OutputMetadata
359385
return nil
360386
}
361387
i.OutputMetadata = outputsMetadata.Metadata
388+
if len(i.OutputMetadata) > MaxTokenPayloadSize {
389+
return errors.Errorf("output metadata too large: %d > %d", len(i.OutputMetadata), MaxTokenPayloadSize)
390+
}
362391
i.Receivers = slices.GenericSliceOfPointers[AuditableIdentity](len(outputsMetadata.Receivers))
363392
if err := protos.FromProtosSlice(outputsMetadata.Receivers, i.Receivers); err != nil {
364393
return errors.Wrap(err, "failed unmarshalling receivers metadata")
@@ -411,6 +440,15 @@ func (i *IssueMetadata) ToProtos() (*request.IssueMetadata, error) {
411440
}
412441

413442
func (i *IssueMetadata) FromProtos(issueMetadata *request.IssueMetadata) error {
443+
if issueMetadata == nil {
444+
return errors.New("issue metadata is nil")
445+
}
446+
if len(issueMetadata.Inputs) > MaxActionCount {
447+
return errors.Errorf("too many issue inputs: %d > %d", len(issueMetadata.Inputs), MaxActionCount)
448+
}
449+
if len(issueMetadata.Outputs) > MaxActionCount {
450+
return errors.Errorf("too many issue outputs: %d > %d", len(issueMetadata.Outputs), MaxActionCount)
451+
}
414452
issuer := &AuditableIdentity{}
415453
if err := issuer.FromProtos(issueMetadata.Issuer); err != nil {
416454
return errors.Wrapf(err, "failed unmarshalling issuer [%v]", issueMetadata.Issuer)
@@ -507,7 +545,13 @@ func (t *TransferOutputMetadata) FromProtos(transferOutputMetadata *request.Outp
507545
return nil
508546
}
509547
t.OutputMetadata = transferOutputMetadata.Metadata
548+
if len(t.OutputMetadata) > MaxTokenPayloadSize {
549+
return errors.Errorf("output metadata too large: %d > %d", len(t.OutputMetadata), MaxTokenPayloadSize)
550+
}
510551
t.OutputAuditInfo = transferOutputMetadata.AuditInfo
552+
if len(t.OutputAuditInfo) > MaxOwnerRawSize {
553+
return errors.Errorf("output audit info too large: %d > %d", len(t.OutputAuditInfo), MaxOwnerRawSize)
554+
}
511555
t.Receivers = slices.GenericSliceOfPointers[AuditableIdentity](len(transferOutputMetadata.Receivers))
512556
if err := protos.FromProtosSlice(transferOutputMetadata.Receivers, t.Receivers); err != nil {
513557
return errors.Wrap(err, "failed unmarshalling receivers metadata")
@@ -577,6 +621,15 @@ func (t *TransferMetadata) ToProtos() (*request.TransferMetadata, error) {
577621
}
578622

579623
func (t *TransferMetadata) FromProtos(transferMetadata *request.TransferMetadata) error {
624+
if transferMetadata == nil {
625+
return errors.New("transfer metadata is nil")
626+
}
627+
if len(transferMetadata.Inputs) > MaxActionCount {
628+
return errors.Errorf("too many transfer inputs: %d > %d", len(transferMetadata.Inputs), MaxActionCount)
629+
}
630+
if len(transferMetadata.Outputs) > MaxActionCount {
631+
return errors.Errorf("too many transfer outputs: %d > %d", len(transferMetadata.Outputs), MaxActionCount)
632+
}
580633
t.Inputs = slices.GenericSliceOfPointers[TransferInputMetadata](len(transferMetadata.Inputs))
581634
if err := protos.FromProtosSlice(transferMetadata.Inputs, t.Inputs); err != nil {
582635
return errors.Wrap(err, "failed unmarshalling inputs")
@@ -712,6 +765,10 @@ func (m *TokenRequestMetadata) FromProtos(trm *request.TokenRequestMetadata) err
712765
return errors.Errorf("invalid token request metadata version, expected [%d], got [%d]", ProtocolV1, trm.Version)
713766
}
714767

768+
if len(trm.Metadata) > MaxActionCount {
769+
return errors.Errorf("too many action metadata: %d > %d", len(trm.Metadata), MaxActionCount)
770+
}
771+
715772
m.Application = trm.Application
716773
for _, meta := range trm.Metadata {
717774
im := meta.GetIssueMetadata()

token/driver/validator.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ type ValidatorLedger interface {
4545
GetState(id token.ID) ([]byte, error)
4646
}
4747

48+
// ValidationConfig defines the limits for token validation operations to prevent resource exhaustion.
49+
type ValidationConfig struct {
50+
MaxTokenPayloadSize int
51+
MaxTokenOutputsPerTx int
52+
MaxBulkDeleteSize int
53+
MaxWalletIDSize int
54+
MaxOwnerRawSize int
55+
MaxIssuerRawSize int
56+
MaxTokenRequestSize int
57+
MaxActionCount int
58+
}
59+
4860
// Validator provides methods for validating token transaction requests.
4961
// It ensures that requests are well-formed and consistent with the rules
5062
// defined by the token driver.
@@ -68,4 +80,7 @@ type Validator interface {
6880
// Setting this to 0 (default) accepts all protocol versions.
6981
// This is useful for enforcing protocol upgrades across a network.
7082
SetMinProtocolVersion(version uint32)
83+
84+
// SetValidationConfig configures the validation limits for the validator.
85+
SetValidationConfig(config ValidationConfig)
7186
}

0 commit comments

Comments
 (0)