Merge SEC-05: body size limit and max password length
This commit is contained in:
@@ -1269,9 +1269,21 @@ func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxJSONBytes limits the size of JSON request bodies (1 MiB).
|
||||||
|
//
|
||||||
|
// Security (SEC-05): without a size limit an attacker could send a
|
||||||
|
// multi-gigabyte body and exhaust server memory. The UI layer already
|
||||||
|
// applies http.MaxBytesReader; this constant gives the REST API the
|
||||||
|
// same protection.
|
||||||
|
const maxJSONBytes = 1 << 20
|
||||||
|
|
||||||
// decodeJSON decodes a JSON request body into v.
|
// decodeJSON decodes a JSON request body into v.
|
||||||
// Returns false and writes a 400 response if decoding fails.
|
// Returns false and writes a 400 response if decoding fails.
|
||||||
|
//
|
||||||
|
// Security (SEC-05): the body is wrapped with http.MaxBytesReader so
|
||||||
|
// that oversized payloads are rejected before they are fully read.
|
||||||
func decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) bool {
|
func decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) bool {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBytes)
|
||||||
dec := json.NewDecoder(r.Body)
|
dec := json.NewDecoder(r.Body)
|
||||||
dec.DisallowUnknownFields()
|
dec.DisallowUnknownFields()
|
||||||
if err := dec.Decode(v); err != nil {
|
if err := dec.Decode(v); err != nil {
|
||||||
|
|||||||
@@ -594,3 +594,21 @@ func TestRenewToken(t *testing.T) {
|
|||||||
t.Error("old token should be revoked after renewal")
|
t.Error("old token should be revoked after renewal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOversizedJSONBodyRejected(t *testing.T) {
|
||||||
|
srv, _, _, _ := newTestServer(t)
|
||||||
|
handler := srv.Handler()
|
||||||
|
|
||||||
|
// Build a JSON body larger than 1 MiB.
|
||||||
|
oversized := bytes.Repeat([]byte("A"), (1<<20)+1)
|
||||||
|
body := []byte(`{"username":"admin","password":"` + string(oversized) + `"}`)
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/v1/auth/login", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if rr.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("expected 400 for oversized body, got %d", rr.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,11 +45,22 @@ func Username(username string) error {
|
|||||||
// password.
|
// password.
|
||||||
const MinPasswordLen = 12
|
const MinPasswordLen = 12
|
||||||
|
|
||||||
// Password returns nil if the plaintext password meets the minimum length
|
// MaxPasswordLen is the maximum acceptable plaintext password length.
|
||||||
// requirement, or a descriptive error if not.
|
//
|
||||||
|
// 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 {
|
func Password(password string) error {
|
||||||
if len(password) < MinPasswordLen {
|
if len(password) < MinPasswordLen {
|
||||||
return fmt.Errorf("password must be at least %d characters", 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ func TestPasswordTooShort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPasswordTooLong(t *testing.T) {
|
||||||
|
// Exactly MaxPasswordLen should be accepted.
|
||||||
|
if err := Password(strings.Repeat("a", MaxPasswordLen)); err != nil {
|
||||||
|
t.Errorf("Password(len=%d) = %v, want nil", MaxPasswordLen, err)
|
||||||
|
}
|
||||||
|
// One over the limit should be rejected.
|
||||||
|
if err := Password(strings.Repeat("a", MaxPasswordLen+1)); err == nil {
|
||||||
|
t.Errorf("Password(len=%d) = nil, want error", MaxPasswordLen+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUsernameValid(t *testing.T) {
|
func TestUsernameValid(t *testing.T) {
|
||||||
valid := []string{
|
valid := []string{
|
||||||
"alice",
|
"alice",
|
||||||
|
|||||||
Reference in New Issue
Block a user