mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 23:00:55 +03:00
add support for a basic built-in defender
It can help to prevent DoS and brute force password guessing
This commit is contained in:
@@ -692,12 +692,14 @@ func TestBasicUsersCache(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _, _, err = server.authenticate(req) //nolint:dogsled
|
||||
ipAddr := "127.0.0.1"
|
||||
|
||||
_, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled
|
||||
assert.Error(t, err)
|
||||
|
||||
now := time.Now()
|
||||
req.SetBasicAuth(username, password)
|
||||
_, isCached, _, err := server.authenticate(req)
|
||||
_, isCached, _, err := server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
// now the user should be cached
|
||||
@@ -708,14 +710,14 @@ func TestBasicUsersCache(t *testing.T) {
|
||||
assert.False(t, cachedUser.IsExpired())
|
||||
assert.True(t, cachedUser.Expiration.After(now.Add(time.Duration(c.Cache.Users.ExpirationTime)*time.Minute)))
|
||||
// authenticate must return the cached user now
|
||||
authUser, isCached, _, err := server.authenticate(req)
|
||||
authUser, isCached, _, err := server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isCached)
|
||||
assert.Equal(t, cachedUser.User, authUser)
|
||||
}
|
||||
// a wrong password must fail
|
||||
req.SetBasicAuth(username, "wrong")
|
||||
_, _, _, err = server.authenticate(req) //nolint:dogsled
|
||||
_, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled
|
||||
assert.EqualError(t, err, dataprovider.ErrInvalidCredentials.Error())
|
||||
req.SetBasicAuth(username, password)
|
||||
|
||||
@@ -728,7 +730,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||
assert.True(t, cachedUser.IsExpired())
|
||||
}
|
||||
// now authenticate should get the user from the data provider and update the cache
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
result, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||
@@ -742,7 +744,7 @@ func TestBasicUsersCache(t *testing.T) {
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||
assert.False(t, ok)
|
||||
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(username)
|
||||
@@ -808,24 +810,25 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||
server, err := newServer(c, configDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ipAddr := "127.0.1.1"
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user1.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user1.Username, password+"1")
|
||||
_, isCached, _, err := server.authenticate(req)
|
||||
_, isCached, _, err := server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user2.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user2.Username, password+"2")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user3.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user3.Username, password+"3")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
|
||||
@@ -840,7 +843,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user4.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user4.Username, password+"4")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
// user1, the first cached, should be removed now
|
||||
@@ -857,7 +860,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user1.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user1.Username, password+"1")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)
|
||||
@@ -873,7 +876,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user2.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user2.Username, password+"2")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)
|
||||
@@ -889,7 +892,7 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user3.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user3.Username, password+"3")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
|
||||
@@ -910,14 +913,14 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user4.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user4.Username, password+"4")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user1.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(user1.Username, password+"1")
|
||||
_, isCached, _, err = server.authenticate(req)
|
||||
_, isCached, _, err = server.authenticate(req, ipAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isCached)
|
||||
_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)
|
||||
|
||||
@@ -121,11 +121,16 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
checkRemoteAddress(r)
|
||||
if err := common.Config.ExecutePostConnectHook(r.RemoteAddr, common.ProtocolWebDAV); err != nil {
|
||||
ipAddr := utils.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if common.IsBanned(ipAddr) {
|
||||
http.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
user, _, lockSystem, err := s.authenticate(r)
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolWebDAV); err != nil {
|
||||
http.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
user, _, lockSystem, err := s.authenticate(r, ipAddr)
|
||||
if err != nil {
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"SFTPGo WebDAV\"")
|
||||
http.Error(w, err401.Error(), http.StatusUnauthorized)
|
||||
@@ -139,19 +144,19 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
connectionID, err := s.validateUser(user, r)
|
||||
if err != nil {
|
||||
updateLoginMetrics(user.Username, r.RemoteAddr, err)
|
||||
updateLoginMetrics(user.Username, ipAddr, err)
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
fs, err := user.GetFilesystem(connectionID)
|
||||
if err != nil {
|
||||
updateLoginMetrics(user.Username, r.RemoteAddr, err)
|
||||
updateLoginMetrics(user.Username, ipAddr, err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
updateLoginMetrics(user.Username, r.RemoteAddr, err)
|
||||
updateLoginMetrics(user.Username, ipAddr, err)
|
||||
|
||||
ctx := context.WithValue(r.Context(), requestIDKey, connectionID)
|
||||
ctx = context.WithValue(ctx, requestStartKey, time.Now())
|
||||
@@ -177,7 +182,7 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
||||
func (s *webDavServer) authenticate(r *http.Request) (dataprovider.User, bool, webdav.LockSystem, error) {
|
||||
func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.User, bool, webdav.LockSystem, error) {
|
||||
var user dataprovider.User
|
||||
var err error
|
||||
username, password, ok := r.BasicAuth()
|
||||
@@ -193,13 +198,13 @@ func (s *webDavServer) authenticate(r *http.Request) (dataprovider.User, bool, w
|
||||
if len(password) > 0 && cachedUser.Password == password {
|
||||
return cachedUser.User, true, cachedUser.LockSystem, nil
|
||||
}
|
||||
updateLoginMetrics(username, r.RemoteAddr, dataprovider.ErrInvalidCredentials)
|
||||
updateLoginMetrics(username, ip, dataprovider.ErrInvalidCredentials)
|
||||
return user, false, nil, dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
user, err = dataprovider.CheckUserAndPass(username, password, utils.GetIPFromRemoteAddress(r.RemoteAddr), common.ProtocolWebDAV)
|
||||
user, err = dataprovider.CheckUserAndPass(username, password, ip, common.ProtocolWebDAV)
|
||||
if err != nil {
|
||||
updateLoginMetrics(username, r.RemoteAddr, err)
|
||||
updateLoginMetrics(username, ip, err)
|
||||
return user, false, nil, err
|
||||
}
|
||||
lockSystem := webdav.NewMemLS()
|
||||
@@ -315,11 +320,15 @@ func checkRemoteAddress(r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func updateLoginMetrics(username, remoteAddress string, err error) {
|
||||
func updateLoginMetrics(username, ip string, err error) {
|
||||
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
||||
ip := utils.GetIPFromRemoteAddress(remoteAddress)
|
||||
if err != nil {
|
||||
logger.ConnectionFailedLog(username, ip, dataprovider.LoginMethodPassword, common.ProtocolWebDAV, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||
event = common.HostEventUserNotFound
|
||||
}
|
||||
common.AddDefenderEvent(ip, event)
|
||||
}
|
||||
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err)
|
||||
dataprovider.ExecutePostLoginHook(username, dataprovider.LoginMethodPassword, ip, common.ProtocolWebDAV, err)
|
||||
|
||||
@@ -121,7 +121,11 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
common.Initialize(commonConf)
|
||||
err = common.Initialize(commonConf)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("error initializing common: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = dataprovider.Initialize(providerConf, configDir)
|
||||
if err != nil {
|
||||
@@ -502,6 +506,49 @@ func TestLoginInvalidPwd(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginNonExistentUser(t *testing.T) {
|
||||
user := getTestUser()
|
||||
client := getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
}
|
||||
|
||||
func TestDefender(t *testing.T) {
|
||||
oldConfig := config.GetCommonConfig()
|
||||
|
||||
cfg := config.GetCommonConfig()
|
||||
cfg.DefenderConfig.Enabled = true
|
||||
cfg.DefenderConfig.Threshold = 3
|
||||
|
||||
err := common.Initialize(cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
client := getWebDavClient(user)
|
||||
assert.NoError(t, checkBasicFunc(client))
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
user.Password = "wrong_pwd"
|
||||
client = getWebDavClient(user)
|
||||
assert.Error(t, checkBasicFunc(client))
|
||||
}
|
||||
|
||||
user.Password = defaultPassword
|
||||
client = getWebDavClient(user)
|
||||
err = checkBasicFunc(client)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "403")
|
||||
}
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = common.Initialize(oldConfig)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoginInvalidURL(t *testing.T) {
|
||||
u := getTestUser()
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
|
||||
Reference in New Issue
Block a user