Skip to content

Commit ad977d3

Browse files
feat: EdDSA token for database leakage/index mitigation (#971)
* feat: EdDSA token for database leakage/index mitigation * [skip ci]: remove token from api output * fix: e2e tests * fixup! fix: e2e tests * doc: document tokens are now optional fields for app and aclient * fixup(doc): swagger version again * address review comments * address review comments
1 parent 8221c0b commit ad977d3

54 files changed

Lines changed: 633 additions & 422 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/application.go

Lines changed: 4 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
}
@@ -141,6 +143,7 @@ func (a *ApplicationAPI) GetApplications(ctx *gin.Context) {
141143
return
142144
}
143145
for _, app := range apps {
146+
app.Token = ""
144147
withResolvedImage(app)
145148
}
146149
ctx.JSON(200, apps)
@@ -452,11 +455,6 @@ func withResolvedImage(app *model.Application) *model.Application {
452455
return app
453456
}
454457

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

api/application_test.go

Lines changed: 61 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

@@ -263,6 +278,8 @@ func (s *ApplicationSuite) Test_GetApplications() {
263278
assert.Equal(s.T(), 200, s.recorder.Code)
264279
first.Image = "static/defaultapp.png"
265280
second.Image = "static/defaultapp.png"
281+
first.Token = ""
282+
second.Token = ""
266283
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
267284
}
268285

@@ -281,14 +298,16 @@ func (s *ApplicationSuite) Test_GetApplications_WithImage() {
281298
assert.Equal(s.T(), 200, s.recorder.Code)
282299
first.Image = "image/abcd.jpg"
283300
second.Image = "static/defaultapp.png"
301+
first.Token = ""
302+
second.Token = ""
284303
test.BodyEquals(s.T(), []*model.Application{first, second}, s.recorder)
285304
}
286305

287306
func (s *ApplicationSuite) Test_DeleteApplication_internal_expectBadRequest() {
288307
s.db.User(5).InternalApp(10)
289308

290309
test.WithUser(s.ctx, 5)
291-
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
310+
s.ctx.Request = httptest.NewRequest("DELETE", "/token/", nil)
292311
s.ctx.Params = gin.Params{{Key: "id", Value: "10"}}
293312

294313
s.a.DeleteApplication(s.ctx)
@@ -300,7 +319,7 @@ func (s *ApplicationSuite) Test_DeleteApplication_expectNotFound() {
300319
s.db.User(5)
301320

302321
test.WithUser(s.ctx, 5)
303-
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
322+
s.ctx.Request = httptest.NewRequest("DELETE", "/token/", nil)
304323
s.ctx.Params = gin.Params{{Key: "id", Value: "4"}}
305324

306325
s.a.DeleteApplication(s.ctx)
@@ -312,7 +331,7 @@ func (s *ApplicationSuite) Test_DeleteApplication() {
312331
s.db.User(5).App(1)
313332

314333
test.WithUser(s.ctx, 5)
315-
s.ctx.Request = httptest.NewRequest("DELETE", "/token/"+firstApplicationToken, nil)
334+
s.ctx.Request = httptest.NewRequest("DELETE", "/token/", nil)
316335
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
317336

318337
s.a.DeleteApplication(s.ctx)
@@ -371,7 +390,7 @@ func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_expectSuccess() {
371390
imgName := app.Image
372391

373392
assert.Equal(s.T(), 200, s.recorder.Code)
374-
_, err = os.Stat(imgName)
393+
_, err = os.Stat(s.imageDir.Path(imgName))
375394
assert.Nil(s.T(), err)
376395

377396
s.a.DeleteApplication(s.ctx)
@@ -381,55 +400,26 @@ func (s *ApplicationSuite) Test_UploadAppImage_WithImageFile_expectSuccess() {
381400
}
382401
}
383402

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

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

400416
s.a.UploadApplicationImage(s.ctx)
401417

402418
assert.Equal(s.T(), 200, s.recorder.Code)
403419

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")})
420+
listing, err := os.ReadDir(s.imageDir.Path())
419421
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")
422+
assert.Len(s.T(), listing, 1)
433423
}
434424

435425
func (s *ApplicationSuite) Test_UploadAppImage_WithTextFile_expectBadRequest() {
@@ -504,14 +494,14 @@ func (s *ApplicationSuite) Test_RemoveAppImage_expectSuccess() {
504494

505495
imageFile := "existing.png"
506496
s.db.CreateApplication(&model.Application{UserID: 5, ID: 1, Image: imageFile})
507-
fakeImage(s.T(), imageFile)
497+
fakeImage(s.T(), s.imageDir.Path(imageFile))
508498

509499
test.WithUser(s.ctx, 5)
510500
s.ctx.Request = httptest.NewRequest("DELETE", "/irrelevant", nil)
511501
s.ctx.Params = gin.Params{{Key: "id", Value: "1"}}
512502
s.a.RemoveApplicationImage(s.ctx)
513503

514-
_, err := os.Stat(imageFile)
504+
_, err := os.Stat(s.imageDir.Path(imageFile))
515505
assert.True(s.T(), os.IsNotExist(err))
516506

517507
assert.Equal(s.T(), 200, s.recorder.Code)
@@ -672,7 +662,7 @@ func (s *ApplicationSuite) withFormData(formData string) {
672662
s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
673663
}
674664

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

api/client.go

Lines changed: 4 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
}
@@ -198,6 +200,7 @@ func (a *ClientAPI) GetClients(ctx *gin.Context) {
198200
}
199201
now := time.Now()
200202
for _, client := range clients {
203+
client.Token = ""
201204
if client.ElevatedUntil != nil && !now.Before(*client.ElevatedUntil) {
202205
client.ElevatedUntil = nil
203206
}
@@ -321,8 +324,3 @@ func (a *ClientAPI) ElevateClient(ctx *gin.Context) {
321324
ctx.Status(204)
322325
})
323326
}
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)