Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions api/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/gin-gonic/gin"
"github.com/gotify/server/v3/auth"
"github.com/gotify/server/v3/fracdex"
"github.com/gotify/server/v3/model"
"github.com/h2non/filetype"
"gorm.io/gorm"
Expand Down Expand Up @@ -94,6 +95,9 @@ type ApplicationParams struct {
func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
applicationParams := ApplicationParams{}
if err := ctx.Bind(&applicationParams); err == nil {
if !validateSortKey(ctx, applicationParams.SortKey) {
return
}
tokenPublic, tokenPrivate := generateApplicationToken()
app := model.Application{
Name: applicationParams.Name,
Expand Down Expand Up @@ -263,6 +267,9 @@ func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) {
if app != nil && app.UserID == auth.GetUserID(ctx) {
applicationParams := ApplicationParams{}
if err := ctx.Bind(&applicationParams); err == nil {
if !validateSortKey(ctx, applicationParams.SortKey) {
return
}
app.Description = applicationParams.Description
app.Name = applicationParams.Name
app.DefaultPriority = applicationParams.DefaultPriority
Expand Down Expand Up @@ -602,6 +609,14 @@ func ValidApplicationImageExt(ext string) bool {
}
}

func validateSortKey(ctx *gin.Context, sortKey string) bool {
if err := fracdex.ValidateOrderKey(sortKey); err != nil {
ctx.AbortWithError(400, err)
return false
}
return true
}

func handleApplicationError(ctx *gin.Context, err error) {
if errors.Is(err, gorm.ErrDuplicatedKey) {
ctx.AbortWithError(400, errors.New("sort key is not unique"))
Expand Down
58 changes: 58 additions & 0 deletions api/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,32 @@ func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnEmptyName()
}
}

func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnTooLongSortKey() {
s.db.User(5)

test.WithUser(s.ctx, 5)
s.withFormData("name=app&sortKey=" + strings.Repeat("z", 256))
s.a.CreateApplication(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
if app, err := s.db.GetApplicationsByUser(5); assert.NoError(s.T(), err) {
assert.Empty(s.T(), app)
}
}

func (s *ApplicationSuite) Test_CreateApplication_expectBadRequestOnInvalidSortKey() {
s.db.User(5)

test.WithUser(s.ctx, 5)
s.withFormData("name=app&sortKey=a10")
s.a.CreateApplication(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
if app, err := s.db.GetApplicationsByUser(5); assert.NoError(s.T(), err) {
assert.Empty(s.T(), app)
}
}

func (s *ApplicationSuite) Test_CreateApplication_ignoresReadOnlyPropertiesInParams() {
s.db.User(5)

Expand Down Expand Up @@ -764,6 +790,38 @@ func (s *ApplicationSuite) Test_UpdateApplication_duplicateSortKey() {
assert.Equal(s.T(), 400, s.recorder.Code)
}

func (s *ApplicationSuite) Test_UpdateApplication_expectBadRequestOnTooLongSortKey() {
user := s.db.User(5)
user.App(1) // sortKey=a0

s.withFormData("name=new_name&sortKey=" + strings.Repeat("z", 256))
test.WithUser(s.ctx, 5)
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}

s.a.UpdateApplication(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
assert.Equal(s.T(), "a0", app.SortKey)
}
}

func (s *ApplicationSuite) Test_UpdateApplication_expectBadRequestOnInvalidSortKey() {
user := s.db.User(5)
user.App(1) // sortKey=a0

s.withFormData("name=new_name&sortKey=a10")
test.WithUser(s.ctx, 5)
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}

s.a.UpdateApplication(s.ctx)

assert.Equal(s.T(), 400, s.recorder.Code)
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
assert.Equal(s.T(), "a0", app.SortKey)
}
}

func (s *ApplicationSuite) withFormData(formData string) {
s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(formData))
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
Expand Down
44 changes: 32 additions & 12 deletions fracdex/fracdex.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,31 @@ const (
base62Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
smallestInt = "A00000000000000000000000000"
zero = "a0"
maxKeyLen = 255
)

// KeyBetween returns a key that sorts lexicographically between a and b.
// Either a or b can be empty strings. If a is empty it indicates smallest key,
// If b is empty it indicates largest key.
// b must be empty string or > a.
func KeyBetween(a, b string) (string, error) {
if a != "" {
err := validateOrderKey(a)
if err != nil {
return "", err
}
if err := ValidateOrderKey(a); err != nil {
return "", err
}
if b != "" {
err := validateOrderKey(b)
if err != nil {
return "", err
}
if err := ValidateOrderKey(b); err != nil {
return "", err
}
key, err := keyBetweenInternal(a, b)
if err != nil {
return "", err
}
if len(key) > maxKeyLen {
return "", errors.New("range overflow")
}
return key, nil
}

func keyBetweenInternal(a, b string) (string, error) {
if a != "" && b != "" && a >= b {
return "", fmt.Errorf("%s >= %s", a, b)
}
Expand All @@ -57,6 +63,9 @@ func KeyBetween(a, b string) (string, error) {
if res == "" {
return "", errors.New("range underflow")
}
if res == smallestInt {
return res + midpoint("", ""), nil
}
return res, nil
}

Expand Down Expand Up @@ -192,7 +201,13 @@ func getIntPart(key string) (string, error) {
return key[0:intPartLen], nil
}

func validateOrderKey(key string) error {
func ValidateOrderKey(key string) error {
if key == "" {
return nil
}
if len(key) > maxKeyLen {
return fmt.Errorf("invalid order key: too long")
}
if key == smallestInt {
return fmt.Errorf("invalid order key: %s", key)
}
Expand All @@ -203,6 +218,11 @@ func validateOrderKey(key string) error {
if err != nil {
return err
}
for _, c := range key {
if !strings.ContainsRune(base62Digits, c) {
return fmt.Errorf("invalid order key: %s", key)
}
}
f := key[len(i):]
if strings.HasSuffix(f, "0") {
return fmt.Errorf("invalid order key: %s", key)
Expand Down Expand Up @@ -294,7 +314,7 @@ func Float64Approx(key string) (float64, error) {
return 0.0, errors.New("invalid order key")
}

err := validateOrderKey(key)
err := ValidateOrderKey(key)
if err != nil {
return 0.0, err
}
Expand Down
59 changes: 59 additions & 0 deletions fracdex/fracdex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,62 @@ func TestToFloat64Approx(t *testing.T) {
test("a400", 0.0, "invalid order key: a400")
test("a!", 0.0, "invalid order key: a!")
}

func FuzzValidateOrderKey(f *testing.F) {
for _, seed := range [][2]string{
{"", ""},
{"a0", "a1"},
{"Zz", "a0"},
{"a0V", "a1"},
{"", "Y00"},
{"bzz", ""},
{"b125", "b129"},
{"zzzzzzzzzzzzzzzzzzzzzzzzzzz", ""},
{"", "A00000000000000000000000001"},
{strings.Repeat("z", 255), strings.Repeat("z", 256)},
{"a0 ", "a0!"},
} {
f.Add(seed[0], seed[1])
}

f.Fuzz(func(t *testing.T, a, b string) {
// Validating must never panic
if ValidateOrderKey(a) != nil || ValidateOrderKey(b) != nil {
return
}

// Any valid key must be safe to use with other fracdex functions.
checkBetween := func(lo, hi string) {
if lo != "" && hi != "" && lo >= hi {
return
}
mid, err := KeyBetween(lo, hi)
if err != nil {
return
}
assert.NoError(t, ValidateOrderKey(mid), "KeyBetween(%q, %q) = %q", lo, hi, mid)
assert.NotEmpty(t, mid, "KeyBetween(%q, %q)", lo, hi)
if lo != "" {
assert.Greater(t, mid, lo, "KeyBetween(%q, %q) = %q", lo, hi, mid)
}
if hi != "" {
assert.Less(t, mid, hi, "KeyBetween(%q, %q) = %q", lo, hi, mid)
}
}

checkBetween(a, "")
checkBetween("", a)
checkBetween(b, "")
checkBetween("", b)
checkBetween(a, b)
checkBetween(b, a)

for _, key := range []string{a, b} {
if key == "" {
continue
}
_, err := Float64Approx(key)
assert.NoError(t, err, "Float64Approx(%q)", key)
}
})
}
Loading