Files
mcias/internal/validate/validate.go
Kyle Isom 70e4f715f7 Fix SEC-05: add body size limit to REST API and max password length
- 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>
2026-03-13 00:42:11 -07:00

67 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 1255 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
}