add support for a basic built-in defender

It can help to prevent DoS and brute force password guessing
This commit is contained in:
Nicola Murino
2021-01-02 14:05:09 +01:00
parent 30eb3c4a99
commit 037d89a320
23 changed files with 1530 additions and 131 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)