import: the export file held to the forms' rules before anything is created (audit M10)
test / test (push) Canceled after 0s

domain.Import trusted the file for everything but the domain name, the
selector and the password: DMARCRua was not validate.Email'ed, client IPs
not parsed, a rate limit's mode, window and count not checked - a window of
0 counted nothing, a garbage IP made the application refuse every client.

handlers.checkDomainImport runs in HandleImportDomain after the domain name
check and before Import, and refuses the file with "Invalid <what> in export
file: <the form's own text>": the report address (validate.Email), each
rate limit (parseRateLimitMode; auto - checkAutoMultiplier, 0 meaning the
default as an empty field does; manual - checkManualLimit, which the form
now uses too: count > 0, <= the level-1 backstop, window > 0; both 0 is an
export without a limit of its own), each application's client addresses
and the legacy ones on its rate limit (parseIPList, written back as the
form stores them; allow-list on with none is refused). Login, sending
addresses and password stay checked in app.ImportApplication.

Tests: a good export passes and its addresses come out normalised; nine
broken fields each refused with their message; a refused file posted to the
handler creates no domain.

CHANGELOG [Unreleased], audit plan line ticked, roadmap 18/62.
Evidence: go vet clean; the import, rate-limit and application tests of
./internal/web/handlers ok; the server run follows before the push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-14 15:28:29 +03:00
co-authored by Claude Opus 5.5
parent f9e75de8d1
commit e469034477
6 changed files with 211 additions and 11 deletions
+30 -9
View File
@@ -45,10 +45,31 @@ func parseAutoMultiplier(raw string) (float64, error) {
if err != nil {
return 0, fmt.Errorf("enter a valid multiplier (%.1f–%.1f)", store.MinAutoMultiplier, store.MaxAutoMultiplier)
}
return v, checkAutoMultiplier(v)
}
// checkAutoMultiplier is the rule for an auto limit's multiplier, shared by
// the form and a domain import.
func checkAutoMultiplier(v float64) error {
if v < store.MinAutoMultiplier || v > store.MaxAutoMultiplier {
return 0, fmt.Errorf("multiplier must be between %.1f and %.1f", store.MinAutoMultiplier, store.MaxAutoMultiplier)
return fmt.Errorf("multiplier must be between %.1f and %.1f", store.MinAutoMultiplier, store.MaxAutoMultiplier)
}
return v, nil
return nil
}
// checkManualLimit is the rule for a manual limit, shared by the form and a
// domain import: a window of no seconds would count nothing.
func checkManualLimit(maxMessages, windowSeconds, l1Max int) error {
if maxMessages <= 0 {
return fmt.Errorf("enter a message limit greater than zero")
}
if maxMessages > l1Max {
return fmt.Errorf("message limit cannot exceed the level-1 backstop (%d)", l1Max)
}
if windowSeconds <= 0 {
return fmt.Errorf("enter a time window greater than zero seconds")
}
return nil
}
func parseRateLimitMode(raw string) (string, error) {
@@ -86,15 +107,15 @@ func parseRateLimitForm(r *http.Request, l1Max int) (rateLimitInput, error) {
return rateLimitInput{clear: true}, nil
}
maxMessages, err := parsePositiveInt(rawMax, 0)
if err != nil || maxMessages <= 0 {
return rateLimitInput{}, fmt.Errorf("enter a message limit greater than zero")
}
if maxMessages > l1Max {
return rateLimitInput{}, fmt.Errorf("message limit cannot exceed the level-1 backstop (%d)", l1Max)
if err != nil {
maxMessages = 0
}
windowSeconds, err := parsePositiveInt(r.PostFormValue("window_seconds"), defaultRateLimitWindowSeconds)
if err != nil || windowSeconds <= 0 {
return rateLimitInput{}, fmt.Errorf("enter a time window greater than zero seconds")
if err != nil {
windowSeconds = 0
}
if err := checkManualLimit(maxMessages, windowSeconds, l1Max); err != nil {
return rateLimitInput{}, err
}
return rateLimitInput{mode: store.RateLimitModeManual, maxMessages: maxMessages, windowSeconds: windowSeconds}, nil
}