You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by ti...@apache.org on 2020/07/14 11:41:18 UTC
[servicecomb-service-center] branch master updated: block user
brute force login attempts (#666)
This is an automated email from the ASF dual-hosted git repository.
tianxiaoliang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-service-center.git
The following commit(s) were added to refs/heads/master by this push:
new ed18b91 block user brute force login attempts (#666)
ed18b91 is described below
commit ed18b91158a3306d8901829954024bad4d45dc8c
Author: Shawn <xi...@gmail.com>
AuthorDate: Tue Jul 14 19:41:08 2020 +0800
block user brute force login attempts (#666)
---
go.mod | 1 +
server/plugin/auth/buildin/buildin.go | 1 -
server/rest/controller/v4/auth_resource.go | 21 ++++-
server/rest/controller/v4/auth_resource_test.go | 44 ++++++++++-
server/service/rbac/blocker.go | 100 ++++++++++++++++++++++++
server/service/rbac/blocker_test.go | 74 ++++++++++++++++++
server/service/rbac/password.go | 3 +
test/test.go | 13 +--
8 files changed, 242 insertions(+), 15 deletions(-)
diff --git a/go.mod b/go.mod
index 491a601..e7e9e9c 100644
--- a/go.mod
+++ b/go.mod
@@ -76,6 +76,7 @@ require (
go.etcd.io/etcd v3.3.22+incompatible
go.uber.org/zap v1.10.0
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586
+ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
google.golang.org/grpc v1.19.0
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/karlseguin/expect.v1 v1.0.1 // indirect
diff --git a/server/plugin/auth/buildin/buildin.go b/server/plugin/auth/buildin/buildin.go
index d3cf120..7a9f91a 100644
--- a/server/plugin/auth/buildin/buildin.go
+++ b/server/plugin/auth/buildin/buildin.go
@@ -50,7 +50,6 @@ func (ba *TokenAuthenticator) Identify(req *http.Request) error {
if !rbacframe.MustAuth(req.URL.Path) {
return nil
}
-
v := req.Header.Get(restful.HeaderAuth)
if v == "" {
return rbacframe.ErrNoHeader
diff --git a/server/rest/controller/v4/auth_resource.go b/server/rest/controller/v4/auth_resource.go
index dd0cfeb..70ab5b4 100644
--- a/server/rest/controller/v4/auth_resource.go
+++ b/server/rest/controller/v4/auth_resource.go
@@ -24,6 +24,7 @@ import (
"github.com/apache/servicecomb-service-center/pkg/log"
"github.com/apache/servicecomb-service-center/pkg/rbacframe"
"github.com/apache/servicecomb-service-center/pkg/rest"
+ "github.com/apache/servicecomb-service-center/pkg/util"
"github.com/apache/servicecomb-service-center/server/rest/controller"
"github.com/apache/servicecomb-service-center/server/scerror"
"github.com/apache/servicecomb-service-center/server/service"
@@ -75,6 +76,12 @@ func (r *AuthResource) CreateAccount(w http.ResponseWriter, req *http.Request) {
}
}
func (r *AuthResource) ChangePassword(w http.ResponseWriter, req *http.Request) {
+ ip := util.GetRealIP(req)
+ if rbac.IsBanned(ip) {
+ log.Warn("ip is banned:" + ip)
+ controller.WriteError(w, scerror.ErrForbidden, "")
+ return
+ }
body, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Error("read body err", err)
@@ -101,11 +108,16 @@ func (r *AuthResource) ChangePassword(w http.ResponseWriter, req *http.Request)
err = rbac.ChangePassword(context.TODO(), changer.Role, changer.Name, a)
if err != nil {
if err == rbac.ErrSamePassword ||
- err == rbac.ErrWrongPassword || err == rbac.ErrEmptyCurrentPassword ||
+ err == rbac.ErrEmptyCurrentPassword ||
err == rbac.ErrNoPermChangeAccount {
controller.WriteError(w, scerror.ErrInvalidParams, err.Error())
return
}
+ if err == rbac.ErrWrongPassword {
+ rbac.CountFailure(ip)
+ controller.WriteError(w, scerror.ErrInvalidParams, err.Error())
+ return
+ }
log.Error("change password failed", err)
controller.WriteError(w, scerror.ErrInternal, err.Error())
return
@@ -113,6 +125,12 @@ func (r *AuthResource) ChangePassword(w http.ResponseWriter, req *http.Request)
}
func (r *AuthResource) Login(w http.ResponseWriter, req *http.Request) {
+ ip := util.GetRealIP(req)
+ if rbac.IsBanned(ip) {
+ log.Warn("ip is banned:" + ip)
+ controller.WriteError(w, scerror.ErrForbidden, "")
+ return
+ }
body, err := ioutil.ReadAll(req.Body)
if err != nil {
log.Error("read body err", err)
@@ -129,6 +147,7 @@ func (r *AuthResource) Login(w http.ResponseWriter, req *http.Request) {
if err != nil {
if err == rbac.ErrUnauthorized {
log.Error("not authorized", err)
+ rbac.CountFailure(ip)
controller.WriteError(w, scerror.ErrUnauthorized, err.Error())
return
}
diff --git a/server/rest/controller/v4/auth_resource_test.go b/server/rest/controller/v4/auth_resource_test.go
index 4a693d4..273ad35 100644
--- a/server/rest/controller/v4/auth_resource_test.go
+++ b/server/rest/controller/v4/auth_resource_test.go
@@ -12,12 +12,14 @@ import (
"github.com/astaxie/beego"
"github.com/go-chassis/go-archaius"
"github.com/go-chassis/go-chassis/security/secret"
+ "github.com/go-chassis/go-chassis/server/restful"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
+ _ "github.com/apache/servicecomb-service-center/server/handler/auth"
_ "github.com/apache/servicecomb-service-center/test"
)
@@ -80,17 +82,53 @@ func TestAuthResource_Login(t *testing.T) {
rest.GetRouter().ServeHTTP(w, r)
assert.Equal(t, http.StatusUnauthorized, w.Code)
})
- t.Run("dev_account login and change pwd", func(t *testing.T) {
+ t.Run("dev_account login and change pwd,then login again", func(t *testing.T) {
b, _ := json.Marshal(&rbacframe.Account{Name: "dev_account", Password: "Complicated_password1"})
r, _ := http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
w := httptest.NewRecorder()
rest.GetRouter().ServeHTTP(w, r)
assert.Equal(t, http.StatusOK, w.Code)
- jsonbody := w.Body.Bytes()
to := &rbacframe.Token{}
- json.Unmarshal(jsonbody, to)
+ json.Unmarshal(w.Body.Bytes(), to)
+ b2, _ := json.Marshal(&rbacframe.Account{CurrentPassword: "Complicated_password1", Password: "Complicated_password2"})
+ r, _ = http.NewRequest(http.MethodPost, "/v4/account/dev_account/password", bytes.NewBuffer(b2))
+ r.Header.Set(restful.HeaderAuth, "Bearer "+to.TokenStr)
+ w = httptest.NewRecorder()
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusOK, w.Code)
+
+ b3, _ := json.Marshal(&rbacframe.Account{Name: "dev_account", Password: "Complicated_password2"})
+ r, _ = http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b3))
+ w = httptest.NewRecorder()
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusOK, w.Code)
})
+ t.Run("bock user dev_account", func(t *testing.T) {
+ b, _ := json.Marshal(&rbacframe.Account{Name: "dev_account", Password: "Complicated_password1"})
+ r, _ := http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
+ w := httptest.NewRecorder()
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+
+ r, _ = http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+
+ r, _ = http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+
+ r, _ = http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+
+ w = httptest.NewRecorder()
+ r, _ = http.NewRequest(http.MethodPost, "/v4/token", bytes.NewBuffer(b))
+ rest.GetRouter().ServeHTTP(w, r)
+ assert.Equal(t, http.StatusForbidden, w.Code)
+
+ })
}
diff --git a/server/service/rbac/blocker.go b/server/service/rbac/blocker.go
new file mode 100644
index 0000000..8d94b46
--- /dev/null
+++ b/server/service/rbac/blocker.go
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rbac
+
+import (
+ "sync"
+ "time"
+
+ "golang.org/x/time/rate"
+)
+
+const (
+ MaxAttempts = 5
+
+ BlockInterval = 1 * time.Hour
+)
+
+var BanTime = 1 * time.Hour
+
+type Client struct {
+ limiter *rate.Limiter
+ Key string
+ Banned bool
+ ReleaseAt time.Time //at this time client can be allow to attempt to do something
+}
+
+var clients sync.Map
+
+func BannedList() []*Client {
+ cs := make([]*Client, 0)
+ clients.Range(func(key, value interface{}) bool {
+ client := value.(*Client)
+ if client.Banned && time.Now().After(client.ReleaseAt) {
+ client.Banned = false
+ client.ReleaseAt = time.Time{}
+ return true
+ }
+ cs = append(cs, client)
+ return true
+ })
+ return cs
+}
+
+//CountFailure can cause a client banned
+// it use time/rate to allow certainty failure,
+//but will ban client if rate limiter can not accept failures
+func CountFailure(key string) {
+ var c interface{}
+ var client *Client
+ var ok bool
+ now := time.Now()
+ if c, ok = clients.Load(key); !ok {
+ client = &Client{
+ Key: key,
+ limiter: rate.NewLimiter(rate.Every(BlockInterval), MaxAttempts),
+ ReleaseAt: time.Time{},
+ }
+ clients.Store(key, client)
+ } else {
+ client = c.(*Client)
+ }
+
+ allow := client.limiter.AllowN(time.Now(), 1)
+ if !allow {
+ client.Banned = true
+ client.ReleaseAt = now.Add(BanTime)
+ }
+}
+
+//IsBanned check if a client is banned, and if client ban time expire,
+//it will release the client from banned status
+func IsBanned(key string) bool {
+ var c interface{}
+ var client *Client
+ var ok bool
+ if c, ok = clients.Load(key); !ok {
+ return false
+ }
+ client = c.(*Client)
+ if client.Banned && time.Now().After(client.ReleaseAt) {
+ client.Banned = false
+ client.ReleaseAt = time.Time{}
+ }
+ return client.Banned
+}
diff --git a/server/service/rbac/blocker_test.go b/server/service/rbac/blocker_test.go
new file mode 100644
index 0000000..6e77746
--- /dev/null
+++ b/server/service/rbac/blocker_test.go
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package rbac_test
+
+import (
+ "github.com/apache/servicecomb-service-center/server/service/rbac"
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func TestCountFailure(t *testing.T) {
+ rbac.BanTime = 3 * time.Second
+ rbac.CountFailure("1")
+ assert.False(t, rbac.IsBanned("1"))
+
+ rbac.CountFailure("1")
+ assert.False(t, rbac.IsBanned("1"))
+
+ rbac.CountFailure("1")
+ assert.False(t, rbac.IsBanned("1"))
+
+ rbac.CountFailure("1")
+ assert.False(t, rbac.IsBanned("1"))
+
+ rbac.CountFailure("1")
+ assert.False(t, rbac.IsBanned("1"))
+
+ rbac.CountFailure("1")
+ assert.True(t, rbac.IsBanned("1"))
+
+ t.Run("ban 1 more", func(t *testing.T) {
+ rbac.CountFailure("2")
+ assert.False(t, rbac.IsBanned("2"))
+
+ rbac.CountFailure("2")
+ assert.False(t, rbac.IsBanned("2"))
+
+ rbac.CountFailure("2")
+ assert.False(t, rbac.IsBanned("2"))
+
+ rbac.CountFailure("2")
+ assert.False(t, rbac.IsBanned("2"))
+
+ rbac.CountFailure("2")
+ assert.False(t, rbac.IsBanned("2"))
+
+ rbac.CountFailure("2")
+ assert.True(t, rbac.IsBanned("2"))
+ })
+ t.Log(rbac.BannedList()[0].ReleaseAt)
+ assert.Equal(t, 2, len(rbac.BannedList()))
+
+ time.Sleep(4 * time.Second)
+ assert.Equal(t, 0, len(rbac.BannedList()))
+ assert.False(t, rbac.IsBanned("1"))
+ assert.False(t, rbac.IsBanned("2"))
+
+}
diff --git a/server/service/rbac/password.go b/server/service/rbac/password.go
index 971f6c2..37b5875 100644
--- a/server/service/rbac/password.go
+++ b/server/service/rbac/password.go
@@ -92,5 +92,8 @@ func doChangePassword(ctx context.Context, old *rbacframe.Account, pwd string) e
func SamePassword(hashedPwd, pwd string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPwd), []byte(pwd))
+ if err == bcrypt.ErrMismatchedHashAndPassword {
+ log.Warn("incorrect password attempts")
+ }
return err == nil
}
diff --git a/test/test.go b/test/test.go
index 650bab0..e7cf9d7 100644
--- a/test/test.go
+++ b/test/test.go
@@ -19,19 +19,12 @@
package test
import (
- mgr "github.com/apache/servicecomb-service-center/server/plugin"
- "github.com/apache/servicecomb-service-center/server/plugin/discovery/etcd"
- etcd2 "github.com/apache/servicecomb-service-center/server/plugin/registry/etcd"
- plain "github.com/apache/servicecomb-service-center/server/plugin/security/buildin"
- "github.com/apache/servicecomb-service-center/server/plugin/tracing/pzipkin"
+ _ "github.com/apache/servicecomb-service-center/server/bootstrap"
+ "github.com/apache/servicecomb-service-center/server/core"
"github.com/astaxie/beego"
)
func init() {
beego.AppConfig.Set("registry_plugin", "etcd")
- mgr.RegisterPlugin(mgr.Plugin{mgr.REGISTRY, "etcd", etcd2.NewRegistry})
- mgr.RegisterPlugin(mgr.Plugin{mgr.DISCOVERY, "buildin", etcd.NewRepository})
- mgr.RegisterPlugin(mgr.Plugin{mgr.DISCOVERY, "etcd", etcd.NewRepository})
- mgr.RegisterPlugin(mgr.Plugin{mgr.CIPHER, "buildin", plain.New})
- mgr.RegisterPlugin(mgr.Plugin{mgr.TRACING, "buildin", pzipkin.New})
+ core.ServerInfo.Config.MaxBodyBytes = 2097152
}