Explorar el Código

feat(validator): cap entry limit at MaxEntryLimit

Reject API "limit" query parameter and "entries_per_page" preference
values above 1000, and clamp the storage entry query builder so any
caller (REST API, Google Reader, internal UI) is bounded.

The HTML settings form now exposes the cap via the input "max"
attribute.
Frédéric Guillot hace 1 mes
padre
commit
0909323ae3

+ 4 - 0
internal/model/entry.go

@@ -15,6 +15,10 @@ const (
 	DefaultSortingDirection = "asc"
 	DefaultSortingDirection = "asc"
 )
 )
 
 
+// MaxEntryLimit is the maximum allowed value for the "limit" query parameter
+// and for the user "entries_per_page" preference.
+const MaxEntryLimit = 1000
+
 // Entry represents a feed item in the system.
 // Entry represents a feed item in the system.
 type Entry struct {
 type Entry struct {
 	ID          int64         `json:"id"`
 	ID          int64         `json:"id"`

+ 1 - 1
internal/storage/entry_query_builder.go

@@ -216,7 +216,7 @@ func (e *EntryQueryBuilder) WithSorting(column, direction string) *EntryQueryBui
 // WithLimit set the limit.
 // WithLimit set the limit.
 func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
 func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
 	if limit > 0 {
 	if limit > 0 {
-		e.limit = limit
+		e.limit = min(limit, model.MaxEntryLimit)
 	}
 	}
 	return e
 	return e
 }
 }

+ 1 - 1
internal/template/templates/views/settings.html

@@ -204,7 +204,7 @@
         </select>
         </select>
 
 
         <label for="form-entries-per-page">{{ t "form.prefs.label.entries_per_page" }}</label>
         <label for="form-entries-per-page">{{ t "form.prefs.label.entries_per_page" }}</label>
-        <input type="number" name="entries_per_page" id="form-entries-per-page" value="{{ .form.EntriesPerPage }}" min="1">
+        <input type="number" name="entries_per_page" id="form-entries-per-page" value="{{ .form.EntriesPerPage }}" min="1" max="{{ .maxEntriesPerPage }}">
 
 
         <label><input type="checkbox" name="keyboard_shortcuts" value="1" {{ if .form.KeyboardShortcuts }}checked{{ end }}> {{ t "form.prefs.label.keyboard_shortcuts" }}</label>
         <label><input type="checkbox" name="keyboard_shortcuts" value="1" {{ if .form.KeyboardShortcuts }}checked{{ end }}> {{ t "form.prefs.label.keyboard_shortcuts" }}</label>
 
 

+ 1 - 0
internal/ui/settings_show.go

@@ -73,6 +73,7 @@ func (h *handler) showSettingsPage(w http.ResponseWriter, r *http.Request) {
 	view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
 	view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
 	view.Set("default_home_pages", model.HomePages())
 	view.Set("default_home_pages", model.HomePages())
 	view.Set("categories_sorting_options", model.CategoriesSortingOptions())
 	view.Set("categories_sorting_options", model.CategoriesSortingOptions())
+	view.Set("maxEntriesPerPage", model.MaxEntryLimit)
 	view.Set("countWebAuthnCerts", h.store.CountWebAuthnCredentialsByUserID(user.ID))
 	view.Set("countWebAuthnCerts", h.store.CountWebAuthnCredentialsByUserID(user.ID))
 	view.Set("webAuthnCerts", creds)
 	view.Set("webAuthnCerts", creds)
 
 

+ 1 - 1
internal/validator/user.go

@@ -212,7 +212,7 @@ func validateTimezone(timezoneValue string) *locale.LocalizedError {
 }
 }
 
 
 func validateEntriesPerPage(entriesPerPage int) *locale.LocalizedError {
 func validateEntriesPerPage(entriesPerPage int) *locale.LocalizedError {
-	if entriesPerPage < 1 {
+	if entriesPerPage < 1 || entriesPerPage > model.MaxEntryLimit {
 		return locale.NewLocalizedError("error.entries_per_page_invalid")
 		return locale.NewLocalizedError("error.entries_per_page_invalid")
 	}
 	}
 	return nil
 	return nil

+ 5 - 3
internal/validator/user_test.go

@@ -110,11 +110,13 @@ func TestValidateEntryDirection(t *testing.T) {
 }
 }
 
 
 func TestValidateEntriesPerPage(t *testing.T) {
 func TestValidateEntriesPerPage(t *testing.T) {
-	if err := validateEntriesPerPage(1); err != nil {
-		t.Errorf("expected positive entries per page to pass, got %v", err)
+	for _, value := range []int{1, model.MaxEntryLimit} {
+		if err := validateEntriesPerPage(value); err != nil {
+			t.Errorf("expected %d to pass, got %v", value, err)
+		}
 	}
 	}
 
 
-	for _, value := range []int{0, -1} {
+	for _, value := range []int{0, -1, model.MaxEntryLimit + 1} {
 		if err := validateEntriesPerPage(value); err == nil {
 		if err := validateEntriesPerPage(value); err == nil {
 			t.Errorf("expected %d to fail", value)
 			t.Errorf("expected %d to fail", value)
 		}
 		}

+ 7 - 0
internal/validator/validator.go

@@ -5,8 +5,11 @@ package validator // import "miniflux.app/v2/internal/validator"
 
 
 import (
 import (
 	"errors"
 	"errors"
+	"fmt"
 	"regexp"
 	"regexp"
 	"strings"
 	"strings"
+
+	"miniflux.app/v2/internal/model"
 )
 )
 
 
 var domainRegex = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
 var domainRegex = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
@@ -21,6 +24,10 @@ func ValidateRange(offset, limit int) error {
 		return errors.New(`limit value should be >= 0`)
 		return errors.New(`limit value should be >= 0`)
 	}
 	}
 
 
+	if limit > model.MaxEntryLimit {
+		return fmt.Errorf(`limit value should be <= %d`, model.MaxEntryLimit)
+	}
+
 	return nil
 	return nil
 }
 }
 
 

+ 13 - 1
internal/validator/validator_test.go

@@ -3,7 +3,11 @@
 
 
 package validator // import "miniflux.app/v2/internal/validator"
 package validator // import "miniflux.app/v2/internal/validator"
 
 
-import "testing"
+import (
+	"testing"
+
+	"miniflux.app/v2/internal/model"
+)
 
 
 func TestValidateRange(t *testing.T) {
 func TestValidateRange(t *testing.T) {
 	if err := ValidateRange(-1, 0); err == nil {
 	if err := ValidateRange(-1, 0); err == nil {
@@ -14,9 +18,17 @@ func TestValidateRange(t *testing.T) {
 		t.Error(`An invalid limit should generate a error`)
 		t.Error(`An invalid limit should generate a error`)
 	}
 	}
 
 
+	if err := ValidateRange(0, model.MaxEntryLimit+1); err == nil {
+		t.Error(`A limit above MaxEntryLimit should generate an error`)
+	}
+
 	if err := ValidateRange(42, 42); err != nil {
 	if err := ValidateRange(42, 42); err != nil {
 		t.Error(`A valid offset and limit should not generate any error`)
 		t.Error(`A valid offset and limit should not generate any error`)
 	}
 	}
+
+	if err := ValidateRange(0, model.MaxEntryLimit); err != nil {
+		t.Error(`A limit equal to MaxEntryLimit should not generate an error`)
+	}
 }
 }
 
 
 func TestValidateDirection(t *testing.T) {
 func TestValidateDirection(t *testing.T) {