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.
This commit is contained in:
Frédéric Guillot
2026-05-03 17:44:00 -07:00
committed by fguillot
parent 6543d652a6
commit 0909323ae3
8 changed files with 33 additions and 7 deletions
+4
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {