diff --git a/README.md b/README.md index da9ccf16de28..3572484d373b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - [Limit-count](doc/plugins/limit-count.md) - [Limit-concurrency](doc/plugins/limit-conn.md) - Anti-ReDoS(Regular expression Denial of Service): Built-in policies to Anti ReDoS without configuration. - - [CORS](doc/plugins/cors.md) + - [CORS](doc/plugins/cors.md) Enable CORS(Cross-origin resource sharing) for your API. + - [uri-blocker](plugins/uri-blocker.md): Block client request by URI. - **OPS friendly** - OpenTracing: support [Apache Skywalking](doc/plugins/skywalking.md) and [Zipkin](doc/plugins/zipkin.md) diff --git a/README_CN.md b/README_CN.md index 1fbd99814699..408fc2227a1b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -90,7 +90,8 @@ A/B 测试、金丝雀发布(灰度发布)、蓝绿部署、限流限速、抵 - [限制请求数](doc/zh-cn/plugins/limit-count.md) - [限制并发](doc/zh-cn/plugins/limit-conn.md) - 防御 ReDoS(正则表达式拒绝服务):内置策略,无需配置即可抵御 ReDoS。 - - [CORS](doc/zh-cn/plugins/cors.md) + - [CORS](doc/zh-cn/plugins/cors.md):为你的API启用 CORS。 + - [uri-blocker](plugins/uri-blocker.md):根据 URI 拦截用户请求。 - **运维友好** - OpenTracing 可观测性: 支持 [Apache Skywalking](doc/zh-cn/plugins/skywalking.md) 和 [Zipkin](doc/zh-cn/plugins/zipkin.md)。 diff --git a/apisix/plugins/uri-blocker.lua b/apisix/plugins/uri-blocker.lua new file mode 100644 index 000000000000..ab5b6828e772 --- /dev/null +++ b/apisix/plugins/uri-blocker.lua @@ -0,0 +1,86 @@ +-- +-- 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 re_compile = require("resty.core.regex").re_match_compile +local re_find = ngx.re.find +local ipairs = ipairs + +local schema = { + type = "object", + properties = { + block_rules = { + type = "array", + items = { + type = "string", + minLength = 1, + maxLength = 4096, + }, + uniqueItems = true + }, + rejected_code = { + type = "integer", + minimum = 200, + default = 403 + }, + }, + required = {"block_rules"}, +} + + +local plugin_name = "uri-blocker" + +local _M = { + version = 0.1, + priority = 2900, + name = plugin_name, + schema = schema, +} + + +function _M.check_schema(conf) + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + local block_rules = {} + for i, re_rule in ipairs(conf.block_rules) do + local ok, err = re_compile(re_rule, "j") + -- core.log.warn("ok: ", tostring(ok), " err: ", tostring(err), " re_rule: ", re_rule) + if not ok then + return false, err + end + block_rules[i] = re_rule + end + + conf.block_rules_concat = core.table.concat(block_rules, "|") + core.log.info("concat block_rules: ", conf.block_rules_concat) + return true +end + + +function _M.rewrite(conf, ctx) + core.log.info("uri: ", ctx.var.request_uri) + core.log.info("block uri rules: ", conf.block_rules_concat) + local from = re_find(ctx.var.request_uri, conf.block_rules_concat, "jo") + if from then + core.response.exit(conf.rejected_code) + end +end + + +return _M diff --git a/conf/config.yaml b/conf/config.yaml index d640ef79521f..cee95a601019 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -95,7 +95,7 @@ apisix: ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA" key_encrypt_salt: "edd1c9f0985e76a2" # If not set, will save origin ssl key into etcd. # If set this, must be a string of length 16. And it will encrypt ssl key with AES-128-CBC - # !!! So do not change it after saving your ssl, it can't decrypt the ssl keys have be saved if you change !! + # !!! So do not change it after saving your ssl, it can't decrypt the ssl keys have be saved if you change !! # discovery: eureka # service discovery center nginx_config: # config for render the template to genarate nginx.conf error_log: "logs/error.log" @@ -168,6 +168,7 @@ plugins: # plugin list - skywalking - echo - authz-keycloak + - uri-blocker stream_plugins: - mqtt-proxy diff --git a/doc/README.md b/doc/README.md index 042012d8a12a..3f9142adc04d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -65,10 +65,12 @@ Plugins * [kafka-logger](plugins/kafka-logger.md): Log requests to External Kafka servers. * [cors](plugins/cors.md): Enable CORS(Cross-origin resource sharing) for your API. * [batch-requests](plugins/batch-requests.md): Allow you send mutiple http api via **http pipeline**. -* [authz-keycloak](plugins/authz-keycloak.md): Authorization with Keycloak Identity Server +* [authz-keycloak](plugins/authz-keycloak.md): Authorization with Keycloak Identity Server. +* [uri-blocker](plugins/uri-blocker.md): Block client request by URI. Deploy to the Cloud ======= + ### AWS The recommended approach is to deploy APISIX with [AWS CDK](https://aws.amazon.com/cdk/) on [AWS Fargate](https://aws.amazon.com/fargate/) which helps you decouple the APISIX layer and the upstream layer on top of a fully-managed and secure serverless container compute environment with autoscaling capabilities. diff --git a/doc/plugins/uri-blocker.md b/doc/plugins/uri-blocker.md new file mode 100644 index 000000000000..270f3039c2d0 --- /dev/null +++ b/doc/plugins/uri-blocker.md @@ -0,0 +1,96 @@ + + +[Chinese](uri-blocker.md) + +# Summary + +- [**Name**](#name) +- [**Attributes**](#attributes) +- [**How To Enable**](#how-to-enable) +- [**Test Plugin**](#test-plugin) +- [**Disable Plugin**](#disable-plugin) + +## Name + +The plugin helps we intercept user requests, we only need to indicate the `block_rules`. + +## Attributes + +|Name |Requirement |Description| +|--------- |--------|-----------| +|block_rules |required|Regular filter rule array. Each of these items is a regular rule. If the current request URI hits any one of them, set the response code to rejected_code to exit the current user request. Example: `["root.exe", "root.m+"]`.| +|rejected_code |optional|The HTTP status code returned when the request URI hit any of `filter_rule`, default `403`.| + +## How To Enable + +Here's an example, enable the `uri blocker` plugin on the specified route: + +```shell +curl -i http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/*", + "plugins": { + "uri-blocker": { + "block_rules": ["root.exe", "root.m+"] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +## Test Plugin + +```shell +$ curl -i http://127.0.0.1:9080/root.exe?a=a +HTTP/1.1 403 Forbidden +Date: Wed, 17 Jun 2020 13:55:41 GMT +Content-Type: text/html; charset=utf-8 +Content-Length: 150 +Connection: keep-alive +Server: APISIX web server + +... ... +``` + +## Disable Plugin + +When you want to disable the `uri blocker` plugin, it is very simple, + you can delete the corresponding json configuration in the plugin configuration, + no need to restart the service, it will take effect immediately: + +```shell +curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' +{ + "uri": "/*", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } +}' +``` + +The `uri blocker` plugin has been disabled now. It works for other plugins. diff --git a/doc/zh-cn/README.md b/doc/zh-cn/README.md index 2d9965cb33fa..9b2c6c5a0f33 100644 --- a/doc/zh-cn/README.md +++ b/doc/zh-cn/README.md @@ -67,3 +67,4 @@ Reference document * [cors](plugins/cors.md): 为你的API启用 CORS * [batch-requests](plugins/batch-requests.md): 以 **http pipeline** 的方式在网关一次性发起多个 `http` 请求。 * [authz-keycloak](plugins/authz-keycloak-cn.md): 支持 Keycloak 身份认证服务器 +* [uri-blocker](plugins/uri-blocker.md): 根据 URI 拦截用户请求。 diff --git a/t/admin/plugins.t b/t/admin/plugins.t index 1a5f06e8031d..b134b97df731 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -30,7 +30,7 @@ __DATA__ --- request GET /apisix/admin/plugins/list --- response_body_like eval -qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","consumer-restriction","syslog","batch-requests","http-logger","skywalking","echo","authz-keycloak"\]/ +qr/\["limit-req","limit-count","limit-conn","key-auth","basic-auth","prometheus","node-status","jwt-auth","zipkin","ip-restriction","grpc-transcode","serverless-pre-function","serverless-post-function","openid-connect","proxy-rewrite","redirect","response-rewrite","fault-injection","udp-logger","wolf-rbac","proxy-cache","tcp-logger","proxy-mirror","kafka-logger","cors","consumer-restriction","syslog","batch-requests","http-logger","skywalking","echo","authz-keycloak","uri-blocker"\]/ --- no_error_log [error] diff --git a/t/debug/debug-mode.t b/t/debug/debug-mode.t index 2924cdc3e9a6..05534e13e35b 100644 --- a/t/debug/debug-mode.t +++ b/t/debug/debug-mode.t @@ -60,6 +60,7 @@ loaded plugin and sort by priority: 10000 name: serverless-pre-function loaded plugin and sort by priority: 4010 name: batch-requests loaded plugin and sort by priority: 4000 name: cors loaded plugin and sort by priority: 3000 name: ip-restriction +loaded plugin and sort by priority: 2900 name: uri-blocker loaded plugin and sort by priority: 2599 name: openid-connect loaded plugin and sort by priority: 2555 name: wolf-rbac loaded plugin and sort by priority: 2520 name: basic-auth diff --git a/t/plugin/uri-blocker.t b/t/plugin/uri-blocker.t new file mode 100644 index 000000000000..5f10640e4a66 --- /dev/null +++ b/t/plugin/uri-blocker.t @@ -0,0 +1,266 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(2); +no_long_string(); +no_root_location(); +no_shuffle(); + +run_tests; + +__DATA__ + +=== TEST 1: invalid regular expression +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "uri-blocker": { + "block_rules": [".+("] + } + }, + "uri": "/hello" + }]]) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } +} +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin uri-blocker err: pcre_compile() failed: missing ) in \".+(\""} +--- no_error_log +[error] + + + +=== TEST 2: multiple valid rules +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "uri-blocker": { + "block_rules": ["^a", "^b"] + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] +--- error_log +concat block_rules: ^a|^b, + + + +=== TEST 3: multiple rules(include one invalid rule) +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "uri-blocker": { + "block_rules": ["^a", "^b("] + } + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.print(body) + } +} +--- request +GET /t +--- error_code: 400 +--- response_body +{"error_msg":"failed to check the configuration of plugin uri-blocker err: pcre_compile() failed: missing ) in \"^b(\""} +--- no_error_log +[error] + + + +=== TEST 4: sanity +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "uri-blocker": { + "block_rules": ["aa"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]], + [[{ + "node": { + "value": { + "plugins": { + "uri-blocker": { + "block_rules": ["aa"] + } + } + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] +--- error_log +concat block_rules: aa, + + + +=== TEST 5: hit block rule +--- request +GET /hello?aa=1 +--- error_code: 403 +--- no_error_log +[error] + + + +=== TEST 6: miss block rule +--- request +GET /hello?bb=2 +--- no_error_log +[error] + + + +=== TEST 7: multiple block rules +--- config +location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "uri-blocker": { + "block_rules": ["aa", "bb", "c\\d+"] + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } +} +--- request +GET /t +--- response_body +passed +--- no_error_log +[error] +--- error_log +concat block_rules: aa|bb|c\d+, + + + +=== TEST 8: hit block rule +--- request +GET /hello?x=bb +--- error_code: 403 +--- no_error_log +[error] + + + +=== TEST 9: hit block rule +--- request +GET /hello?bb=2 +--- error_code: 403 +--- no_error_log +[error] + + + +=== TEST 10: hit block rule +--- request +GET /hello?c1=2 +--- error_code: 403 +--- no_error_log +[error] + + + +=== TEST 11: not hit block rule +--- request +GET /hello?cc=2 +--- no_error_log +[error]