Skip to content

Commit 709310f

Browse files
committed
More secure handling for download password, better rate limiting
1 parent 8637091 commit 709310f

3 files changed

Lines changed: 96 additions & 18 deletions

File tree

internal/webserver/Webserver.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/forceu/gokapi/internal/storage/presign"
3838
"github.com/forceu/gokapi/internal/webserver/api"
3939
"github.com/forceu/gokapi/internal/webserver/authentication"
40+
"github.com/forceu/gokapi/internal/webserver/authentication/downloadPasswordToken"
4041
"github.com/forceu/gokapi/internal/webserver/authentication/oauth"
4142
"github.com/forceu/gokapi/internal/webserver/authentication/sessionmanager"
4243
"github.com/forceu/gokapi/internal/webserver/authentication/tokengeneration"
@@ -579,26 +580,32 @@ func showDownload(w http.ResponseWriter, r *http.Request) {
579580
}
580581
}
581582

582-
if file.PasswordHash != "" {
583+
if file.PasswordHash != "" && !isValidPwCookie(r, file) {
583584
_ = r.ParseForm()
584585
enteredPassword := r.PostForm.Get("password")
585-
if configuration.HashPassword(enteredPassword, true) != file.PasswordHash && !isValidPwCookie(r, file) {
586-
if enteredPassword != "" {
587-
view.IsFailedLogin = true
588-
time.Sleep(1 * time.Second)
589-
}
586+
if enteredPassword == "" {
590587
view.IsPasswordView = true
591588
err := templateFolder.ExecuteTemplate(w, "download_password", view)
592589
helper.CheckIgnoreTimeout(err)
593590
return
594591
}
595-
if !isValidPwCookie(r, file) {
592+
593+
ip := logging.GetIpAddress(r)
594+
ratelimiter.WaitOnDownloadPassword(ip)
595+
596+
if configuration.HashPassword(enteredPassword, true) == file.PasswordHash {
596597
writeFilePwCookie(w, file)
597598
// redirect so that there is no post data to be resent if user refreshes page
598599
redirect(w, "d?id="+file.Id)
599600
return
600601
}
602+
view.IsFailedLogin = true
603+
view.IsPasswordView = true
604+
err := templateFolder.ExecuteTemplate(w, "download_password", view)
605+
helper.CheckIgnoreTimeout(err)
606+
return
601607
}
608+
602609
err := templateFolder.ExecuteTemplate(w, "download", view)
603610
helper.CheckIgnoreTimeout(err)
604611
}
@@ -1095,23 +1102,21 @@ func newAdminButtonContext(file models.FileApiOutput, user models.User) adminBut
10951102
// Write a cookie if the user has entered a correct password for a password-protected file
10961103
func writeFilePwCookie(w http.ResponseWriter, file models.File) {
10971104
http.SetCookie(w, &http.Cookie{
1098-
Name: "p" + file.Id,
1099-
Value: file.PasswordHash,
1100-
Expires: time.Now().Add(5 * time.Minute),
1105+
Name: "p" + file.Id,
1106+
Value: downloadPasswordToken.Generate(file.Id),
1107+
Expires: time.Now().Add(5 * time.Minute),
1108+
HttpOnly: true,
1109+
SameSite: http.SameSiteStrictMode,
11011110
})
11021111
}
11031112

1104-
// Checks if a cookie contains the correct password hash for a password-protected file
1105-
// If incorrect, a 3-second delay is introduced unless the cookie was empty.
1113+
// Checks if a cookie contains the correct token for a password-protected file
11061114
func isValidPwCookie(r *http.Request, file models.File) bool {
11071115
cookie, err := r.Cookie("p" + file.Id)
1108-
if err == nil {
1109-
if cookie.Value == file.PasswordHash {
1110-
return true
1111-
}
1112-
time.Sleep(3 * time.Second)
1116+
if err != nil {
1117+
return false
11131118
}
1114-
return false
1119+
return downloadPasswordToken.IsValid(cookie.Value, file.Id)
11151120
}
11161121

11171122
// Adds a header to disable external caching
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package downloadPasswordToken
2+
3+
import (
4+
"sync"
5+
"time"
6+
7+
"github.com/forceu/gokapi/internal/helper"
8+
)
9+
10+
var tokens = make(map[string]pwToken)
11+
var mutex sync.Mutex
12+
var cleanupOnce sync.Once
13+
14+
type pwToken struct {
15+
FileId string
16+
Expiry int64
17+
}
18+
19+
const ttl = 5 * time.Minute
20+
21+
func Generate(fileId string) string {
22+
token := helper.GenerateRandomString(60)
23+
mutex.Lock()
24+
tokens[token] = pwToken{
25+
FileId: fileId,
26+
Expiry: time.Now().Add(ttl).Unix(),
27+
}
28+
mutex.Unlock()
29+
30+
cleanupOnce.Do(func() {
31+
go cleanup(true)
32+
})
33+
return token
34+
}
35+
36+
func IsValid(tokenId, fileId string) bool {
37+
mutex.Lock()
38+
defer mutex.Unlock()
39+
token, ok := tokens[tokenId]
40+
if !ok {
41+
return false
42+
}
43+
if token.FileId != fileId {
44+
return false
45+
}
46+
if token.Expiry < time.Now().Unix() {
47+
delete(tokens, tokenId)
48+
return false
49+
}
50+
return true
51+
}
52+
53+
func cleanup(periodic bool) {
54+
mutex.Lock()
55+
for tokenId, token := range tokens {
56+
if token.Expiry < time.Now().Unix() {
57+
delete(tokens, tokenId)
58+
}
59+
}
60+
mutex.Unlock()
61+
if periodic {
62+
time.Sleep(time.Hour)
63+
go cleanup(true)
64+
}
65+
66+
}

internal/webserver/ratelimiter/RateLimiter.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
var newUuidLimiter = newLimiter()
1414
var failedLoginLimiter = newLimiter()
1515
var failedIdLimiter = newLimiter()
16+
var failedDownloadPasswordLimiter = newLimiter()
1617

1718
type limiterEntry struct {
1819
limiter *rate.Limiter
@@ -37,6 +38,12 @@ func WaitOnLogin(ip string) {
3738
_ = failedLoginLimiter.Get(ip, 1, 9).WaitN(context.Background(), 3)
3839
}
3940

41+
// WaitOnDownloadPassword blocks the current goroutine until the rate limiter allows a request
42+
// Ten attempts without limiting, thereafter one attempt every 2 seconds
43+
func WaitOnDownloadPassword(ip string) {
44+
_ = failedLoginLimiter.Get(ip, 1, 20).WaitN(context.Background(), 2)
45+
}
46+
4047
// WaitOnFailedId blocks the current goroutine until the rate limiter allows a request
4148
// Ten failed attempts without limiting, thereafter one attempt every second
4249
func WaitOnFailedId(r *http.Request) {

0 commit comments

Comments
 (0)