- Wrap r.Body with http.MaxBytesReader (1 MiB) in decodeJSON so all REST API endpoints reject oversized JSON payloads - Add MaxPasswordLen = 128 constant and enforce it in validate.Password() to prevent Argon2id DoS via multi-MB passwords - Add test for oversized JSON body rejection (>1 MiB -> 400) - Add test for password max length enforcement Security: decodeJSON now applies the same body size limit the UI layer already uses, closing the asymmetry. MaxPasswordLen caps Argon2id input to a reasonable length, preventing CPU-exhaustion attacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
67 lines
2.5 KiB
Go
67 lines
2.5 KiB
Go
// Package validate provides shared input-validation helpers used by the REST,
|
||
// gRPC, and UI handlers.
|
||
package validate
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
)
|
||
|
||
// usernameRE is the allowed character set for usernames: alphanumeric plus a
|
||
// small set of punctuation that is safe in all contexts (URLs, HTML, logs).
|
||
// Length is enforced separately so the error message can be more precise.
|
||
//
|
||
// Security (F-12): rejecting control characters, null bytes, newlines, and
|
||
// unusual Unicode prevents log injection, stored-XSS via username display,
|
||
// and rendering anomalies in the admin UI.
|
||
var usernameRE = regexp.MustCompile(`^[a-zA-Z0-9._@-]+$`)
|
||
|
||
// MinUsernameLen and MaxUsernameLen are the inclusive bounds on username length.
|
||
const (
|
||
MinUsernameLen = 1
|
||
MaxUsernameLen = 255
|
||
)
|
||
|
||
// Username returns nil if the username is valid, or a descriptive error if not.
|
||
// Valid usernames are 1–255 characters long and contain only alphanumeric
|
||
// characters and the symbols . _ @ -
|
||
func Username(username string) error {
|
||
l := len(username)
|
||
if l < MinUsernameLen || l > MaxUsernameLen {
|
||
return fmt.Errorf("username must be between %d and %d characters", MinUsernameLen, MaxUsernameLen)
|
||
}
|
||
if !usernameRE.MatchString(username) {
|
||
return fmt.Errorf("username may only contain letters, digits, and the characters . _ @ -")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// MinPasswordLen is the minimum acceptable plaintext password length.
|
||
//
|
||
// Security (F-13): NIST SP 800-63B recommends a minimum of 8 characters;
|
||
// we use 12 to provide additional margin against offline brute-force attacks
|
||
// even though Argon2id is expensive. The check is performed at the handler
|
||
// level (before hashing) so Argon2id is never invoked with a trivially weak
|
||
// password.
|
||
const MinPasswordLen = 12
|
||
|
||
// MaxPasswordLen is the maximum acceptable plaintext password length.
|
||
//
|
||
// Security (SEC-05): Argon2id processes the full password input. Without
|
||
// an upper bound an attacker could submit a multi-megabyte password and
|
||
// force expensive hashing. 128 characters is generous for any real
|
||
// password or passphrase while capping the cost.
|
||
const MaxPasswordLen = 128
|
||
|
||
// Password returns nil if the plaintext password meets the length
|
||
// requirements, or a descriptive error if not.
|
||
func Password(password string) error {
|
||
if len(password) < MinPasswordLen {
|
||
return fmt.Errorf("password must be at least %d characters", MinPasswordLen)
|
||
}
|
||
if len(password) > MaxPasswordLen {
|
||
return fmt.Errorf("password must be at most %d characters", MaxPasswordLen)
|
||
}
|
||
return nil
|
||
}
|