Skip to content

Commit 131aa21

Browse files
feat: EdDSA token for database leakage/index mitigation
1 parent 0e32e56 commit 131aa21

39 files changed

Lines changed: 381 additions & 273 deletions

api/application.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,13 @@ type ApplicationParams struct {
9292
func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
9393
applicationParams := ApplicationParams{}
9494
if err := ctx.Bind(&applicationParams); err == nil {
95+
tokenPublic, tokenPrivate := generateApplicationToken()
9596
app := model.Application{
9697
Name: applicationParams.Name,
9798
Description: applicationParams.Description,
9899
DefaultPriority: applicationParams.DefaultPriority,
99100
SortKey: applicationParams.SortKey,
100-
Token: auth.GenerateNotExistingToken(generateApplicationToken, a.applicationExists),
101+
Token: tokenPublic,
101102
UserID: auth.GetUserID(ctx),
102103
Internal: false,
103104
}
@@ -106,6 +107,7 @@ func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) {
106107
handleApplicationError(ctx, err)
107108
return
108109
}
110+
app.Token = tokenPrivate
109111
ctx.JSON(200, withResolvedImage(&app))
110112
}
111113
}
@@ -452,11 +454,6 @@ func withResolvedImage(app *model.Application) *model.Application {
452454
return app
453455
}
454456

455-
func (a *ApplicationAPI) applicationExists(token string) bool {
456-
app, _ := a.DB.GetApplicationByToken(token)
457-
return app != nil
458-
}
459-
460457
func exist(path string) bool {
461458
if _, err := os.Stat(path); os.IsNotExist(err) {
462459
return false

api/application_test.go

Lines changed: 57 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"testing"
1414

1515
"github.com/gin-gonic/gin"
16+
"github.com/gotify/server/v2/auth"
1617
"github.com/gotify/server/v2/mode"
1718
"github.com/gotify/server/v2/model"
1819
"github.com/gotify/server/v2/test"
@@ -22,12 +23,6 @@ import (
2223
"github.com/stretchr/testify/suite"
2324
)
2425

25-
var (
26-
firstApplicationToken = "Aaaaaaaaaaaaaaa"
27-
secondApplicationToken = "Abbbbbbbbbbbbbb"
28-
thirdApplicationToken = "Acccccccccccccc"
29-
)
30-
3126
func TestApplicationSuite(t *testing.T) {
3227
suite.Run(t, new(ApplicationSuite))
3328
}
@@ -37,30 +32,23 @@ type ApplicationSuite struct {
3732
db *testdb.Database
3833
a *ApplicationAPI
3934
ctx *gin.Context
35+
imageDir *test.TmpDir
4036
recorder *httptest.ResponseRecorder
4137
}
4238

43-
var (
44-
originalGenerateApplicationToken func() string
45-
originalGenerateImageName func() string
46-
)
47-
4839
func (s *ApplicationSuite) BeforeTest(suiteName, testName string) {
49-
originalGenerateApplicationToken = generateApplicationToken
50-
originalGenerateImageName = generateImageName
51-
generateApplicationToken = test.Tokens(firstApplicationToken, secondApplicationToken, thirdApplicationToken)
52-
generateImageName = test.Tokens(firstApplicationToken[1:], secondApplicationToken[1:], thirdApplicationToken[1:])
5340
mode.Set(mode.TestDev)
5441
s.recorder = httptest.NewRecorder()
5542
s.db = testdb.NewDB(s.T())
5643
s.ctx, _ = gin.CreateTestContext(s.recorder)
44+
tmpDir := test.NewTmpDir("gotify_applicationsuite")
45+
s.imageDir = &tmpDir
5746
withURL(s.ctx, "http", "example.com")
58-
s.a = &ApplicationAPI{DB: s.db}
47+
s.a = &ApplicationAPI{DB: s.db, ImageDir: s.imageDir.Path() + "/"}
5948
}
6049

6150
func (s *ApplicationSuite) AfterTest(suiteName, testName string) {
62-
generateApplicationToken = originalGenerateApplicationToken
63-
generateImageName = originalGenerateImageName
51+
s.imageDir.Clean()
6452
s.db.Close()
6553
}
6654

@@ -73,7 +61,6 @@ func (s *ApplicationSuite) Test_CreateApplication_mapAllParameters() {
7361

7462
expected := &model.Application{
7563
ID: 1,
76-
Token: firstApplicationToken,
7764
UserID: 5,
7865
Name: "custom_name",
7966
Description: "description_text",
@@ -82,6 +69,7 @@ func (s *ApplicationSuite) Test_CreateApplication_mapAllParameters() {
8269
}
8370
assert.Equal(s.T(), 200, s.recorder.Code)
8471
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
72+
expected.Token = app.Token
8573
assert.Equal(s.T(), expected, app)
8674
}
8775
}
@@ -133,7 +121,6 @@ func (s *ApplicationSuite) Test_CreateApplication_ignoresReadOnlyPropertiesInPar
133121

134122
expected := &model.Application{
135123
ID: 1,
136-
Token: firstApplicationToken,
137124
Name: "name",
138125
Description: "description",
139126
Internal: false,
@@ -143,7 +130,17 @@ func (s *ApplicationSuite) Test_CreateApplication_ignoresReadOnlyPropertiesInPar
143130
}
144131

145132
assert.Equal(s.T(), 200, s.recorder.Code)
146-
test.BodyEquals(s.T(), expected, s.recorder)
133+
bodyBytes, err := io.ReadAll(s.recorder.Body)
134+
assert.Nil(s.T(), err)
135+
var got model.Application
136+
assert.Nil(s.T(), json.Unmarshal(bodyBytes, &got))
137+
expected.Token = got.Token
138+
assert.Equal(s.T(), expected, &got)
139+
tokenParsed, err := auth.ParseEnhancedToken(got.Token)
140+
assert.Nil(s.T(), err)
141+
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
142+
assert.Equal(s.T(), app.Token, tokenParsed.PublicForm())
143+
}
147144
}
148145

149146
func (s *ApplicationSuite) Test_DeleteApplication_expectNotFoundOnCurrentUserIsNotOwner() {
@@ -167,10 +164,18 @@ func (s *ApplicationSuite) Test_CreateApplication_onlyRequiredParameters() {
167164
s.withFormData("name=custom_name")
168165
s.a.CreateApplication(s.ctx)
169166

170-
expected := &model.Application{ID: 1, Token: firstApplicationToken, Name: "custom_name", UserID: 5, SortKey: "a0", CreatedAt: testdb.Now}
167+
expected := &model.Application{ID: 1, Name: "custom_name", SortKey: "a0", CreatedAt: testdb.Now, Image: "static/defaultapp.png"}
171168
assert.Equal(s.T(), 200, s.recorder.Code)
172-
if app, err := s.db.GetApplicationsByUser(5); assert.NoError(s.T(), err) {
173-
assert.Contains(s.T(), app, expected)
169+
bodyBytes, err := io.ReadAll(s.recorder.Body)
170+
assert.Nil(s.T(), err)
171+
var got model.Application
172+
assert.Nil(s.T(), json.Unmarshal(bodyBytes, &got))
173+
expected.Token = got.Token
174+
assert.Equal(s.T(), expected, &got)
175+
tokenParsed, err := auth.ParseEnhancedToken(got.Token)
176+
assert.Nil(s.T(), err)
177+
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
178+
assert.Equal(s.T(), app.Token, tokenParsed.PublicForm())
174179
}
175180
}
176181

@@ -184,29 +189,39 @@ func (s *ApplicationSuite) Test_CreateApplication_returnsApplicationWithID() {
184189

185190
expected := &model.Application{
186191
ID: 1,
187-
Token: firstApplicationToken,
188192
Name: "custom_name",
189193
Image: "static/defaultapp.png",
190194
SortKey: "a0",
191195
CreatedAt: testdb.Now,
192196
}
193197
assert.Equal(s.T(), 200, s.recorder.Code)
194-
test.BodyEquals(s.T(), expected, s.recorder)
198+
bodyBytes, err := io.ReadAll(s.recorder.Body)
199+
assert.Nil(s.T(), err)
200+
var got model.Application
201+
assert.Nil(s.T(), json.Unmarshal(bodyBytes, &got))
202+
expected.Token = got.Token
203+
assert.Equal(s.T(), expected, &got)
204+
tokenParsed, err := auth.ParseEnhancedToken(got.Token)
205+
assert.Nil(s.T(), err)
206+
if app, err := s.db.GetApplicationByID(1); assert.NoError(s.T(), err) {
207+
assert.Equal(s.T(), app.Token, tokenParsed.PublicForm())
208+
}
195209
}
196210

197211
func (s *ApplicationSuite) Test_CreateApplication_withExistingToken() {
198212
s.db.User(5)
199-
s.db.User(6).AppWithToken(1, firstApplicationToken)
213+
s.db.User(6).App(1)
200214

201215
test.WithUser(s.ctx, 5)
202216
s.withFormData("name=custom_name")
203217

204218
s.a.CreateApplication(s.ctx)
205219

206-
expected := &model.Application{ID: 2, Token: secondApplicationToken, Name: "custom_name", UserID: 5, SortKey: "a0", CreatedAt: testdb.Now}
220+
expected := &model.Application{ID: 2, Name: "custom_name", UserID: 5, SortKey: "a0", CreatedAt: testdb.Now}
207221
assert.Equal(s.T(), 200, s.recorder.Code)
208-
if app, err := s.db.GetApplicationsByUser(5); assert.NoError(s.T(), err) {
209-
assert.Contains(s.T(), app, expected)
222+
if app, err := s.db.GetApplicationByID(2); assert.NoError(s.T(), err) {
223+
expected.Token = app.Token
224+
assert.Equal(s.T(), expected, app)
210225
}
211226
}
212227

@@ -288,7 +303,7 @@ func (s *ApplicationSuite) Test_DeleteApplication_internal_expectBadRequest() {
288303
s.db.User(5).InternalApp(10)
289304

290305
test.WithUser(s.ctx, 5)
291-
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
306+
s.ctx.Request = httptest.NewRequest("DELETE", "/token/", nil)
292307
s.ctx.Params = gin.Params{{Key: "id", Value: "10"}}
293308

294309
s.a.DeleteApplication(s.ctx)
@@ -300,7 +315,7 @@ func (s *ApplicationSuite) Test_DeleteApplication_expectNotFound() {
300315
s.db.User(5)
301316

302317
test.WithUser(s.ctx, 5)
303-
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
318+
s.ctx.Request = httptest.NewRequest("DELETE", "/token/", nil)
304319
s.ctx.Params = gin.Params{{Key: "id", Value: "4"}}
305320

306321
s.a.DeleteApplication(s.ctx)
@@ -312,7 +327,7 @@ func (s *ApplicationSuite) Test_DeleteApplication() {
312327
s.db.User(5).App(1)
313328

314329
test.WithUser(s.ctx, 5)
315-
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
330+
s.ctx.Request = httptest.NewRequest("DELETE", "/token/", nil)
316331
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
317332

318333
s.a.DeleteApplication(s.ctx)
@@ -371,7 +386,7 @@ func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_expectSuccess() {
371386
imgName := app.Image
372387

373388
assert.Equal(s.T(), 200, s.recorder.Code)
374-
_, err = os.Stat(imgName)
389+
_, err = os.Stat(s.imageDir.Path(imgName))
375390
assert.Nil(s.T(), err)
376391

377392
s.a.DeleteApplication(s.ctx)
@@ -381,55 +396,26 @@ func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_expectSuccess() {
381396
}
382397
}
383398

384-
func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_DeleteExstingImageAndGenerateNewName() {
385-
existingImageName := "2lHMAel6BDHLL-HrwphcviX-l.png"
386-
firstGeneratedImageName := firstApplicationToken[1:] + ".png"
387-
secondGeneratedImageName := secondApplicationToken[1:] + ".png"
399+
func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_DeleteExstingImage() {
400+
existingImageName := "existing.png"
388401
s.db.User(5)
389402
s.db.CreateApplication(&model.Application{UserID: 5, ID: 1, Image: existingImageName})
403+
fakeImage(s.T(), s.imageDir.Path(existingImageName))
390404

391405
cType, buffer, err := upload(map[string]*os.File{"file": mustOpen("../test/assets/image.png")})
392406
assert.Nil(s.T(), err)
393407
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &buffer)
394408
s.ctx.Request.Header.Set("Content-Type", cType)
395409
test.WithUser(s.ctx, 5)
396410
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
397-
fakeImage(s.T(), existingImageName)
398-
fakeImage(s.T(), firstGeneratedImageName)
399411

400412
s.a.UploadApplicationImage(s.ctx)
401413

402414
assert.Equal(s.T(), 200, s.recorder.Code)
403415

404-
_, err = os.Stat(existingImageName)
405-
assert.True(s.T(), os.IsNotExist(err))
406-
407-
_, err = os.Stat(secondGeneratedImageName)
408-
assert.Nil(s.T(), err)
409-
assert.Nil(s.T(), os.Remove(secondGeneratedImageName))
410-
assert.Nil(s.T(), os.Remove(firstGeneratedImageName))
411-
}
412-
413-
func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_DeleteExistingImage() {
414-
s.db.User(5)
415-
s.db.CreateApplication(&model.Application{UserID: 5, ID: 1, Image: "existing.png"})
416-
417-
fakeImage(s.T(), "existing.png")
418-
cType, buffer, err := upload(map[string]*os.File{"file": mustOpen("../test/assets/image.png")})
416+
listing, err := os.ReadDir(s.imageDir.Path())
419417
assert.Nil(s.T(), err)
420-
s.ctx.Request = httptest.NewRequest("POST", "/irrelevant", &buffer)
421-
s.ctx.Request.Header.Set("Content-Type", cType)
422-
test.WithUser(s.ctx, 5)
423-
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
424-
425-
s.a.UploadApplicationImage(s.ctx)
426-
427-
assert.Equal(s.T(), 200, s.recorder.Code)
428-
429-
_, err = os.Stat("existing.png")
430-
assert.True(s.T(), os.IsNotExist(err))
431-
432-
os.Remove(firstApplicationToken[1:] + ".png")
418+
assert.Len(s.T(), listing, 1)
433419
}
434420

435421
func (s *ApplicationSuite) Test_UploadAppImage_WithTextFile_expectBadRequest() {
@@ -504,14 +490,14 @@ func (s *ApplicationSuite) Test_RemoveAppImage_expectSuccess() {
504490

505491
imageFile := "existing.png"
506492
s.db.CreateApplication(&model.Application{UserID: 5, ID: 1, Image: imageFile})
507-
fakeImage(s.T(), imageFile)
493+
fakeImage(s.T(), s.imageDir.Path(imageFile))
508494

509495
test.WithUser(s.ctx, 5)
510496
s.ctx.Request = httptest.NewRequest("DELETE", "/irrelevant", nil)
511497
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
512498
s.a.RemoveApplicationImage(s.ctx)
513499

514-
_, err := os.Stat(imageFile)
500+
_, err := os.Stat(s.imageDir.Path(imageFile))
515501
assert.True(s.T(), os.IsNotExist(err))
516502

517503
assert.Equal(s.T(), 200, s.recorder.Code)
@@ -672,7 +658,7 @@ func (s *ApplicationSuite) withFormData(formData string) {
672658
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
673659
}
674660

675-
func (s *ApplicationSuite) withJSON(value interface{}) {
661+
func (s *ApplicationSuite) withJSON(value any) {
676662
jsonVal, _ := json.Marshal(value)
677663
s.ctx.Request = httptest.NewRequest("POST", "/application", bytes.NewBuffer(jsonVal))
678664
s.ctx.Request.Header.Set("Content-Type", "application/json")

api/client.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,10 @@ func (a *ClientAPI) UpdateClient(ctx *gin.Context) {
150150
func (a *ClientAPI) CreateClient(ctx *gin.Context) {
151151
clientParams := ClientParams{}
152152
if err := ctx.Bind(&clientParams); err == nil {
153+
tokenPublic, tokenPrivate := generateClientToken()
153154
client := model.Client{
154155
Name: clientParams.Name,
155-
Token: auth.GenerateNotExistingToken(generateClientToken, a.clientExists),
156+
Token: tokenPublic,
156157
UserID: auth.GetUserID(ctx),
157158
}
158159
if clientParams.ExpiresAfterInactivitySeconds != nil {
@@ -162,6 +163,7 @@ func (a *ClientAPI) CreateClient(ctx *gin.Context) {
162163
if success := successOrAbort(ctx, 500, a.DB.CreateClient(&client)); !success {
163164
return
164165
}
166+
client.Token = tokenPrivate
165167
ctx.JSON(200, client)
166168
}
167169
}
@@ -321,8 +323,3 @@ func (a *ClientAPI) ElevateClient(ctx *gin.Context) {
321323
ctx.Status(204)
322324
})
323325
}
324-
325-
func (a *ClientAPI) clientExists(token string) bool {
326-
client, _ := a.DB.GetClientByToken(token)
327-
return client != nil
328-
}

0 commit comments

Comments
 (0)