diff --git a/quicktunnelauth/validation.go b/quicktunnelauth/validation.go index f25dcbd0..d98542a2 100644 --- a/quicktunnelauth/validation.go +++ b/quicktunnelauth/validation.go @@ -1,6 +1,7 @@ package quicktunnelauth import ( + "errors" "fmt" "net/mail" "strings" @@ -8,33 +9,48 @@ import ( "golang.org/x/net/idna" ) +const ( + quickTunnelAuthMaxAllowedMailRules = 100 + quickTunnelAuthMaxAllowedMailRuleBytes = 320 +) + // validateQuickTunnelAllowedMail validates and normalizes exact email addresses and // wildcard domains from one or more comma-separated values. func validateQuickTunnelAllowedMail(values []string) (emails, wildcardDomains map[string]struct{}, err error) { + if len(values) == 0 { + return nil, nil, errors.New("allowed mail rule 1 is empty") + } + emails, wildcardDomains = make(map[string]struct{}), make(map[string]struct{}) - for i, rawEntry := range strings.Split(strings.Join(values, ","), ",") { + for ruleIndex, rawEntry := range strings.Split(strings.Join(values, ","), ",") { + rulePosition := ruleIndex + 1 + if rulePosition > quickTunnelAuthMaxAllowedMailRules { + return nil, nil, fmt.Errorf("allowed mail rules exceed the %d-entry limit", quickTunnelAuthMaxAllowedMailRules) + } + if len(rawEntry) > quickTunnelAuthMaxAllowedMailRuleBytes { + return nil, nil, fmt.Errorf( + "allowed mail rule %d exceeds the %d-byte limit", + rulePosition, + quickTunnelAuthMaxAllowedMailRuleBytes, + ) + } + entry := normalizeQuickTunnelEmail(rawEntry) domain, isWildcard := strings.CutPrefix(entry, "*@") switch { case entry == "": - return nil, nil, fmt.Errorf("allowed mail rule %d is empty", i+1) + return nil, nil, fmt.Errorf("allowed mail rule %d is empty", rulePosition) case isWildcard: if !isValidQuickTunnelEmailDomain(domain) { - return nil, nil, fmt.Errorf( - "allowed mail rule %q has an invalid wildcard domain", - rawEntry, - ) + return nil, nil, fmt.Errorf("allowed mail rule %d has an invalid wildcard domain", rulePosition) } wildcardDomains[domain] = struct{}{} default: if !isValidQuickTunnelEmail(entry) { - return nil, nil, fmt.Errorf( - "allowed mail rule %q is not a valid email address", - rawEntry, - ) + return nil, nil, fmt.Errorf("allowed mail rule %d is not a valid email address", rulePosition) } emails[entry] = struct{}{} } diff --git a/quicktunnelauth/validation_test.go b/quicktunnelauth/validation_test.go index feaa5d02..43de02d0 100644 --- a/quicktunnelauth/validation_test.go +++ b/quicktunnelauth/validation_test.go @@ -1,6 +1,7 @@ package quicktunnelauth import ( + "fmt" "strings" "testing" @@ -96,10 +97,45 @@ func TestValidateQuickTunnelAllowedMailReportsInvalidRule(t *testing.T) { values []string expectedError string }{ - {name: "email", values: []string{"first@example.com", "private@example.com/path"}, expectedError: `allowed mail rule "private@example.com/path" is not a valid email address`}, - {name: "wildcard", values: []string{"first@example.com", "*@example.com/path"}, expectedError: `allowed mail rule "*@example.com/path" has an invalid wildcard domain`}, + {name: "email", values: []string{"first@example.com", "private@example.com/path"}, expectedError: "allowed mail rule 2 is not a valid email address"}, + {name: "wildcard", values: []string{"first@example.com", "*@example.com/path"}, expectedError: "allowed mail rule 2 has an invalid wildcard domain"}, } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, _, err := validateQuickTunnelAllowedMail(test.values) + require.EqualError(t, err, test.expectedError) + for _, value := range test.values { + assert.NotContains(t, err.Error(), value) + } + }) + } +} + +func TestValidateQuickTunnelAllowedMailBoundsInput(t *testing.T) { + t.Parallel() + + tooManyRules := make([]string, quickTunnelAuthMaxAllowedMailRules+1) + for index := range tooManyRules { + tooManyRules[index] = "user@example.com" + } + tests := []struct { + name string + values []string + expectedError string + }{ + { + name: "too many rules", + values: tooManyRules, + expectedError: fmt.Sprintf("allowed mail rules exceed the %d-entry limit", quickTunnelAuthMaxAllowedMailRules), + }, + { + name: "rule too long before trimming", + values: []string{strings.Repeat(" ", quickTunnelAuthMaxAllowedMailRuleBytes) + "a"}, + expectedError: fmt.Sprintf("allowed mail rule 1 exceeds the %d-byte limit", quickTunnelAuthMaxAllowedMailRuleBytes), + }, + } for _, test := range tests { t.Run(test.name, func(t *testing.T) { t.Parallel()