chore: Add upper bounds to supplied emails

This prevents users from abusing `--allowed-mail` policies by limiting the nr. of rules to 100 and defining a max per rule of 320 bytes. Validation errors now identify only the rule position and category, preventing recipient values from appearing in logs. Parsing also stops once limits are exceeded while preserving normalisation and deduplication.
This commit is contained in:
Miguel da Costa Martins Marcelino
2026-09-23 15:59:32 +00:00
parent 07084fd5fd
commit cdcd702aa4
2 changed files with 64 additions and 12 deletions
+26 -10
View File
@@ -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{}{}
}
+38 -2
View File
@@ -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{"[email protected]", "[email protected]/path"}, expectedError: `allowed mail rule "[email protected]/path" is not a valid email address`},
{name: "wildcard", values: []string{"[email protected]", "*@example.com/path"}, expectedError: `allowed mail rule "*@example.com/path" has an invalid wildcard domain`},
{name: "email", values: []string{"[email protected]", "[email protected]/path"}, expectedError: "allowed mail rule 2 is not a valid email address"},
{name: "wildcard", values: []string{"[email protected]", "*@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] = "[email protected]"
}
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()