You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by GitBox <gi...@apache.org> on 2022/01/16 06:32:20 UTC

[GitHub] [apisix] bisakhmondal commented on a change in pull request #5727: feat: add CSRF plugin

bisakhmondal commented on a change in pull request #5727:
URL: https://github.com/apache/apisix/pull/5727#discussion_r785396536



##########
File path: docs/en/latest/plugins/csrf.md
##########
@@ -0,0 +1,136 @@
+---
+title: csrf
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Summary
+
+- [**Description**](#description)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+## Description
+
+The `CSRF` plugin based on the `Double Submit Cookie` way, protect your API from CSRF attacks. This plugin considers the `GET`, `HEAD` and `OPTIONS` methods to be safe operations. Therefore calls to the `GET`, `HEAD` and `OPTIONS` methods are not checked for interception.

Review comment:
       I guess it would be better to add a hyperlink at Double Submit Cookie. https://en.wikipedia.org/wiki/Cross-site_request_forgery#Double_Submit_Cookie

##########
File path: apisix/plugins/csrf.lua
##########
@@ -0,0 +1,161 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local resty_sha256 = require("resty.sha256")
+local str = require("resty.string")
+local ngx = ngx
+local ngx_encode_base64 = ngx.encode_base64
+local ngx_decode_base64 = ngx.decode_base64
+local ngx_time = ngx.time
+local cookie_time = ngx.cookie_time
+local math = math
+
+
+local schema = {
+    type = "object",
+    properties = {
+        key = {
+            description = "use to generate csrf token",
+            type = "string",
+        },
+        expires = {
+            description = "expires time(s) for csrf token",
+            type = "integer",
+            default = 7200
+        },
+        name = {
+            description = "the csrf token name",
+            type = "string",
+            default = "apisix-csrf-token"
+        }
+    },
+    required = {"key"}
+}
+
+
+local _M = {
+    version = 0.1,
+    priority = 2980,
+    name = "csrf",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function gen_sign(random, expires, key)
+    local sha256 = resty_sha256:new()
+
+    local sign = "{expires:" .. expires .. ",random:" .. random .. ",key:" .. key .. "}"
+
+    sha256:update(sign)
+    local digest = sha256:final()
+
+    return str.to_hex(digest)

Review comment:
       Please localize the method as from resty.string module we are using this single method.

##########
File path: apisix/plugins/csrf.lua
##########
@@ -0,0 +1,161 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local resty_sha256 = require("resty.sha256")
+local str = require("resty.string")
+local ngx = ngx
+local ngx_encode_base64 = ngx.encode_base64
+local ngx_decode_base64 = ngx.decode_base64
+local ngx_time = ngx.time
+local cookie_time = ngx.cookie_time

Review comment:
       For code style consistency can we use `ngx_cookie_time` instead of `cookie_time`?

##########
File path: docs/en/latest/plugins/csrf.md
##########
@@ -0,0 +1,136 @@
+---
+title: csrf
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Summary
+
+- [**Description**](#description)
+- [**Attributes**](#attributes)
+- [**How To Enable**](#how-to-enable)
+- [**Test Plugin**](#test-plugin)
+- [**Disable Plugin**](#disable-plugin)
+
+## Description
+
+The `CSRF` plugin based on the `Double Submit Cookie` way, protect your API from CSRF attacks. This plugin considers the `GET`, `HEAD` and `OPTIONS` methods to be safe operations. Therefore calls to the `GET`, `HEAD` and `OPTIONS` methods are not checked for interception.
+
+In the following we define `GET`, `HEAD` and `OPTIONS` as the `safe-methods` and those other than these as `unsafe-methods`.
+
+## Attributes
+
+| Name             | Type    | Requirement | Default | Valid | Description                                                  |
+| ---------------- | ------- | ----------- | ------- | ----- | ------------------------------------------------------------ |
+|   name   |  string |    optional    | `apisix-csrf-token`  |    | The name of the token in the generated cookie. |
+| expires |  number | optional | `7200` | | Expiration time(s) of csrf cookie. |
+| key | string | required |  |  | The secret key used to encrypt the cookie. |
+
+## How To Enable
+
+1. Create the route and enable the plugin.
+
+```shell
+curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT-d '
+{
+  "uri": "/hello",
+  "plugins": {
+    "csrf": {
+      "key": "edd1c9f034335f136f87ad84b625c8f1"
+    }
+  },
+  "upstream": {
+    "type": "roundrobin",
+    "nodes": {
+      "127.0.0.1:9001": 1
+    }
+  }
+}'
+```
+
+The route is then protected, and if you access it using methods other than `GET`, you will see that the request was blocked and receive a 401 status code back.
+
+2. Using `GET` requests `/hello`, a cookie with an encrypted token is received in the response. Token name is the `name` field set in the plugin configuration, if not set, the default value is `apisix-csrf-token`.
+
+Please note: We return a new cookie for each request.
+
+3. In subsequent unsafe-methods requests to this route, you need to read the encrypted token from the cookie and append the token to the `request header`, setting the field name to the `name` in the plugin configuration.
+
+## Test Plugin
+
+Direct access to the '/hello' route using a `POST` method will return an error:
+
+```shell
+curl -i http://127.0.0.1:9080/hello -X POST
+
+HTTP/1.1 401 Unauthorized
+...
+{"error_msg":"no csrf token in headers"}
+```
+
+When accessed with a GET request, the correct return and a cookie with an encrypted token are obtained:
+
+```shell
+curl -i http://127.0.0.1:9080/hello
+
+HTTP/1.1 200 OK
+Set-Cookie: apisix-csrf-token=eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ==;path=/;Expires=Mon, 13-Dec-21 09:33:55 GMT
+```
+
+The token needs to be read from the cookie and carried in the request header in subsequent unsafe-methods requests.
+
+For example, use [js-cookie](https://github.com/js-cookie/js-cookie) read cookie and [axios](https://github.com/axios/axios) send request in client:
+
+```js
+const token = Cookie.get('apisix-csrf-token');
+
+const instance = axios.create({
+  headers: {'apisix-csrf-token': token}
+});
+```
+
+You also need to make sure that you carry the cookie.
+
+Use curl send request:
+
+```shell
+curl -i http://127.0.0.1:9080/hello -X POST -H 'apisix-csrf-token: eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ==' -b 'apisix-csrf-token=eyJyYW5kb20iOjAuNjg4OTcyMzA4ODM1NDMsImV4cGlyZXMiOjcyMDAsInNpZ24iOiJcL09uZEF4WUZDZGYwSnBiNDlKREtnbzVoYkJjbzhkS0JRZXVDQm44MG9ldz0ifQ=='
+
+HTTP/1.1 200 OK
+```
+
+## Disable Plugin
+
+Send a request to update the route to disable the plugin:
+
+```shell
+curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
+{
+  "uri": "/hello",
+  "upstream": {
+    "type": "roundrobin",
+    "nodes": {
+      "127.0.0.1:1980": 1
+    }
+  }
+}'
+```
+
+CSRF plugin have been disabled.

Review comment:
       ```suggestion
   The CSRF plugin has been disabled.
   ```

##########
File path: apisix/plugins/csrf.lua
##########
@@ -0,0 +1,161 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local resty_sha256 = require("resty.sha256")
+local str = require("resty.string")
+local ngx = ngx
+local ngx_encode_base64 = ngx.encode_base64
+local ngx_decode_base64 = ngx.decode_base64
+local ngx_time = ngx.time
+local cookie_time = ngx.cookie_time
+local math = math
+
+
+local schema = {
+    type = "object",
+    properties = {
+        key = {
+            description = "use to generate csrf token",
+            type = "string",
+        },
+        expires = {
+            description = "expires time(s) for csrf token",
+            type = "integer",
+            default = 7200
+        },
+        name = {
+            description = "the csrf token name",
+            type = "string",
+            default = "apisix-csrf-token"
+        }
+    },
+    required = {"key"}
+}
+
+
+local _M = {
+    version = 0.1,
+    priority = 2980,
+    name = "csrf",
+    schema = schema,
+}
+
+
+function _M.check_schema(conf)
+    return core.schema.check(schema, conf)
+end
+
+
+local function gen_sign(random, expires, key)
+    local sha256 = resty_sha256:new()
+
+    local sign = "{expires:" .. expires .. ",random:" .. random .. ",key:" .. key .. "}"
+
+    sha256:update(sign)
+    local digest = sha256:final()
+
+    return str.to_hex(digest)
+end
+
+
+local function gen_csrf_token(conf)
+    local random = math.random()
+    local sign = gen_sign(random, conf.expires, conf.key)
+
+    local token = {
+        random = random,
+        expires = conf.expires,
+        sign = sign,
+    }
+
+    local cookie = ngx_encode_base64(core.json.encode(token))
+    return cookie
+end
+
+
+local function check_csrf_token(conf, ctx, token)
+    local token_str = ngx_decode_base64(token)
+    if token_str == nil then
+        core.log.error("csrf token base64 decode error")
+        return false
+    end
+
+    local token_table, err = core.json.decode(token_str)
+    if err then
+        core.log.error("decode token error: ", err)
+        return false
+    end
+
+    local random = token_table["random"]
+    if not random then
+        core.log.error("no random in token")
+        return false
+    end
+
+    local expires = token_table["expires"]
+    if not expires then
+        core.log.error("no expires in token")
+        return false
+    end
+
+    local sign = gen_sign(random, expires, conf.key)
+    if token_table["sign"] ~= sign then
+        core.log.error("Invalid signatures")
+        return false
+    end
+
+    return true
+end
+
+
+function _M.access(conf, ctx)
+    local safe_methods = {"GET", "HEAD", "OPTIONS"}

Review comment:
       Can we extract this out from the access phase and treat it as a constant array?
   `local SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}`




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org