Kaynağa Gözat

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 1 ay önce
ebeveyn
işleme
0909323ae3

+ 4 - 0
internal/model/entry.go

@@ -15,6 +15,10 @@ const (
 	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.
 type Entry struct {
 	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.
 func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
 	if limit > 0 {
-		e.limit = limit
+		e.limit = min(limit, model.MaxEntryLimit)
 	}
 	return e
 }

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

@@ -204,7 +204,7 @@
         </select>
 
         <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>
 

+ 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("default_home_pages", model.HomePages())
 	view.Set("categories_sorting_options", model.CategoriesSortingOptions())
+	view.Set("maxEntriesPerPage", model.MaxEntryLimit)
 	view.Set("countWebAuthnCerts", h.store.CountWebAuthnCredentialsByUserID(user.ID))
 	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 {
-	if entriesPerPage < 1 {
+	if entriesPerPage < 1 || entriesPerPage > model.MaxEntryLimit {
 		return locale.NewLocalizedError("error.entries_per_page_invalid")
 	}
 	return nil

+ 5 - 3
internal/validator/user_test.go

@@ -110,11 +110,13 @@ func TestValidateEntryDirection(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 {
 			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 (
 	"errors"
+	"fmt"
 	"regexp"
 	"strings"
+
+	"miniflux.app/v2/internal/model"
 )
 
 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`)
 	}
 
+	if limit > model.MaxEntryLimit {
+		return fmt.Errorf(`limit value should be <= %d`, model.MaxEntryLimit)
+	}
+
 	return nil
 }
 

+ 13 - 1
internal/validator/validator_test.go

@@ -3,7 +3,11 @@
 
 package validator // import "miniflux.app/v2/internal/validator"
 
-import "testing"
+import (
+	"testing"
+
+	"miniflux.app/v2/internal/model"
+)
 
 func TestValidateRange(t *testing.T) {
 	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`)
 	}
 
+	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 {
 		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) {