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
 }