This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new ece5ccacf feat: add data-mask plugin (#13347)
ece5ccacf is described below
commit ece5ccacf0dd449bf2c5fdf1fe0812c773073fae
Author: AlinsRan <[email protected]>
AuthorDate: Fri May 15 08:51:48 2026 +0800
feat: add data-mask plugin (#13347)
---
apisix/cli/config.lua | 1 +
apisix/cli/ngx_tpl.lua | 2 +
apisix/core/ctx.lua | 1 +
apisix/init.lua | 3 +
apisix/plugins/data-mask.lua | 316 ++++++++++++++++
conf/config.yaml.example | 1 +
docs/en/latest/config.json | 3 +-
docs/en/latest/plugins/data-mask.md | 308 +++++++++++++++
docs/zh/latest/config.json | 3 +-
docs/zh/latest/plugins/data-mask.md | 309 +++++++++++++++
t/APISIX.pm | 4 +-
t/admin/plugins.t | 1 +
t/plugin/data-mask.t | 722 ++++++++++++++++++++++++++++++++++++
13 files changed, 1671 insertions(+), 3 deletions(-)
diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index f6daf6e9d..c42ecbdee 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -226,6 +226,7 @@ local _M = {
"forward-auth",
"opa",
"authz-keycloak",
+ "data-mask",
"proxy-cache",
"body-transformer",
"ai-prompt-template",
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index 6bd33368b..b823bd182 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -626,6 +626,7 @@ http {
set $upstream_scheme 'http';
set $upstream_host $http_host;
set $upstream_uri '';
+ set $request_line '';
{%if allow_admin then%}
{% for _, allow_ip in ipairs(allow_admin) do %}
@@ -791,6 +792,7 @@ http {
set $upstream_scheme 'http';
set $upstream_host $http_host;
set $upstream_uri '';
+ set $request_line '';
set $ctx_ref '';
{% if wasm then %}
diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua
index 1f97cb38f..dda4ccddc 100644
--- a/apisix/core/ctx.lua
+++ b/apisix/core/ctx.lua
@@ -251,6 +251,7 @@ do
upstream_upgrade = true,
upstream_connection = true,
upstream_uri = true,
+ request_line = true,
llm_content_risk_level = true,
apisix_request_id = true,
diff --git a/apisix/init.lua b/apisix/init.lua
index 5bffaa316..cc83c49cf 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -743,6 +743,9 @@ function _M.http_access_phase()
api_ctx.var.real_request_uri = api_ctx.var.request_uri
api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args ..
(api_ctx.var.args or "")
+ -- var.request is read-only; copy to a writable variable so data-mask can
redact query params
+ api_ctx.var.request_line = api_ctx.var.request
+
handle_x_forwarded_headers(api_ctx)
local match_span = tracer.start(ngx_ctx, "http_router_match",
tracer.kind.internal)
diff --git a/apisix/plugins/data-mask.lua b/apisix/plugins/data-mask.lua
new file mode 100644
index 000000000..de79e740a
--- /dev/null
+++ b/apisix/plugins/data-mask.lua
@@ -0,0 +1,316 @@
+--
+-- 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 ngx = ngx
+local ipairs = ipairs
+local next = next
+local type = type
+local re_sub = ngx.re.sub
+local core = require("apisix.core")
+local jp = require("jsonpath")
+
+local plugin_name = "data-mask"
+
+local schema = {
+ type = "object",
+ properties = {
+ request = {
+ type = "array",
+ items = {
+ type = "object",
+ properties = {
+ type = {type = "string", enum = {"query", "header",
"body"}},
+ body_format = {type = "string", enum = {"json",
"urlencoded"}},
+ name = {type = "string"},
+ action = {type = "string", enum = {"regex", "replace",
"remove"}},
+ regex = {type = "string"},
+ value = {type = "string"},
+ },
+ required = {"type", "name", "action"},
+ allOf = {
+ {
+ ["if"] = {
+ properties = {type = {const = "body"}},
+ },
+ ["then"] = {
+ required = {"body_format"},
+ },
+ },
+ {
+ ["if"] = {
+ properties = {action = {const = "regex"}},
+ },
+ ["then"] = {
+ required = {"regex", "value"},
+ },
+ },
+ {
+ ["if"] = {
+ properties = {action = {const = "replace"}},
+ },
+ ["then"] = {
+ required = {"value"},
+ },
+ },
+ },
+ },
+ },
+ max_body_size = {
+ type = "integer",
+ exclusiveMinimum = 0,
+ default = 1024 * 1024,
+ },
+ max_req_post_args = {
+ type = "integer",
+ default = 100,
+ minimum = 0,
+ }
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1500,
+ 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
+ return true
+end
+
+
+local function regex_replace(origin, regex, new)
+ local res, n, err = re_sub(origin, regex, new, "jo")
+ if not res then
+ core.log.error("failed to apply regex substitution: ", err)
+ end
+ return res, n
+end
+
+
+local function mask_table(tab, conf)
+ if not tab[conf.name] then
+ return false
+ end
+ local masked = false
+ if conf.action == "remove" then
+ tab[conf.name] = nil
+ masked = true
+ elseif conf.action == "replace" then
+ tab[conf.name] = conf.value
+ masked = true
+ elseif conf.action == "regex" then
+ local val = tab[conf.name]
+ if type(val) == "table" then
+ -- same parameter appeared multiple times; apply regex to each
entry
+ for i, v in ipairs(val) do
+ if type(v) ~= "string" then
+ core.log.warn("data-mask: skipping regex for non-string
value in field: ",
+ conf.name)
+ else
+ local new_v, n = regex_replace(v, conf.regex, conf.value)
+ if new_v ~= nil and n > 0 then
+ val[i] = new_v
+ masked = true
+ end
+ end
+ end
+ else
+ if type(val) ~= "string" then
+ core.log.warn("data-mask: skipping regex for non-string value
in field: ",
+ conf.name)
+ else
+ local new_val, n = regex_replace(val, conf.regex, conf.value)
+ if new_val ~= nil and n > 0 then
+ tab[conf.name] = new_val
+ masked = true
+ end
+ end
+ end
+ end
+ return masked
+end
+
+
+-- jsonpath index of array starts from 0, lua table index starts from 1
+local function table_index(idx)
+ if type(idx) == "number" then
+ return idx + 1
+ end
+ return idx
+end
+
+
+local function mask_json(obj, conf)
+ -- local nodes = jp.nodes(data, '$..author')
+ -- {
+ -- { path = {'$', 'store', 'book', 0, 'author'}, value = 'Nigel Rees' },
+ -- { path = {'$', 'store', 'book', 1, 'author'}, value = 'Evelyn Waugh'
},
+ -- }
+ local nodes = jp.nodes(obj, conf.name)
+ if not nodes then
+ return false
+ end
+
+ local masked = false
+ for _, node in ipairs(nodes) do
+ local nested = obj
+ -- first element is root($), last element is the field name
+ for i = 2, #node.path - 1 do
+ nested = nested[table_index(node.path[i])]
+ end
+ local index = table_index(node.path[#node.path])
+ if conf.action == "remove" then
+ nested[index] = nil
+ masked = true
+ elseif conf.action == "replace" then
+ nested[index] = conf.value
+ masked = true
+ elseif conf.action == "regex" then
+ if type(node.value) ~= "string" then
+ core.log.warn("data-mask: skipping regex for non-string value
at path: ",
+ conf.name)
+ else
+ local new_val, n = regex_replace(node.value, conf.regex,
conf.value)
+ if new_val ~= nil and n > 0 then
+ nested[index] = new_val
+ masked = true
+ end
+ end
+ end
+ end
+ return masked
+end
+
+
+function _M.log(conf, ctx)
+ local args = core.request.get_uri_args(ctx)
+ local query_masked = false
+ local post_args
+ local post_args_masked = false
+ local body = ngx.req.get_body_data()
+ local json_body
+ local body_masked = false
+
+ if conf.request then
+ for _, item in ipairs(conf.request) do
+ if item.type == "query" then
+ if mask_table(args, item) then
+ query_masked = true
+ end
+ end
+
+ if item.type == "header" then
+ local header = core.request.header(ctx, item.name)
+ if header then
+ if item.action == "remove" then
+ core.request.set_header(ctx, item.name, nil)
+ elseif item.action == "replace" then
+ core.request.set_header(ctx, item.name, item.value)
+ elseif item.action == "regex" then
+ local new_header, n = regex_replace(header,
item.regex, item.value)
+ if new_header ~= nil and n > 0 then
+ core.request.set_header(ctx, item.name, new_header)
+ end
+ end
+ end
+ end
+
+ if item.type == "body" then
+ if item.body_format == "urlencoded" then
+ if not post_args then
+ if body then
+ local args_err
+ post_args, args_err =
ngx.req.get_post_args(conf.max_req_post_args)
+ if not post_args then
+ core.log.warn("data-mask: failed to get post
args: ", args_err)
+ post_args = {}
+ elseif args_err then
+ core.log.warn("data-mask: post args truncated:
", args_err)
+ end
+ else
+ if ngx.req.get_body_file() then
+ core.log.warn("data-mask: request body is
stored in a " ..
+ "temporary file, body masking will be
skipped")
+ end
+ post_args = {}
+ end
+ end
+ if mask_table(post_args, item) then
+ post_args_masked = true
+ end
+ elseif item.body_format == "json" then
+ if body and #body <= conf.max_body_size then
+ if not json_body then
+ local js, err = core.json.decode(body)
+ if not js then
+ core.log.warn("failed to decode json body: ",
err)
+ else
+ json_body = js
+ end
+ end
+ if json_body then
+ if mask_json(json_body, item) then
+ body_masked = true
+ end
+ end
+ elseif body and #body > conf.max_body_size then
+ core.log.warn("data-mask: skipping body masking for
field '",
+ item.name, "' because body size (", #body,
+ ") exceeds max_body_size (", conf.max_body_size,
")")
+ elseif not body and ngx.req.get_body_file() then
+ core.log.warn("data-mask: request body is stored in a
" ..
+ "temporary file, body masking will be skipped")
+ end
+
+ end
+ end
+ end
+ end
+
+ if query_masked then
+ -- for logger plugins
+ core.request.set_uri_args(ctx, args)
+ if next(args) then
+ ctx.var.request_uri = (ctx.var.uri_before_strip or ctx.var.uri)
+ .. "?" .. core.string.encode_args(args)
+ else
+ ctx.var.request_uri = (ctx.var.uri_before_strip or ctx.var.uri)
+ end
+ -- for access log
+ ctx.var.request_line = core.request.get_method() .. " " ..
ctx.var.request_uri
+ .. " HTTP/" ..
core.request.get_http_version()
+ -- TODO: handle upstream_uri in access log when enable proxy-rewrite
+ end
+
+ if post_args_masked then
+ ngx.req.set_body_data(core.string.encode_args(post_args))
+ end
+
+ if body_masked then
+ ngx.req.set_body_data(core.json.encode(json_body))
+ end
+end
+
+
+return _M
diff --git a/conf/config.yaml.example b/conf/config.yaml.example
index 9922d3c81..6023c83bc 100644
--- a/conf/config.yaml.example
+++ b/conf/config.yaml.example
@@ -509,6 +509,7 @@ plugins: # plugin list (sorted by
priority)
- forward-auth # priority: 2002
- opa # priority: 2001
- authz-keycloak # priority: 2000
+ - data-mask # priority: 1500
#- error-log-logger # priority: 1091
- proxy-cache # priority: 1085
- body-transformer # priority: 1080
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 6de6e1d92..115448b95 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -151,7 +151,8 @@
"plugins/csrf",
"plugins/public-api",
"plugins/gm",
- "plugins/chaitin-waf"
+ "plugins/chaitin-waf",
+ "plugins/data-mask"
]
},
{
diff --git a/docs/en/latest/plugins/data-mask.md
b/docs/en/latest/plugins/data-mask.md
new file mode 100644
index 000000000..fec14abbe
--- /dev/null
+++ b/docs/en/latest/plugins/data-mask.md
@@ -0,0 +1,308 @@
+---
+title: data-mask
+keywords:
+ - Apache APISIX
+ - API Gateway
+ - Plugin
+ - data-mask
+description: This document contains information about the Apache APISIX
data-mask Plugin.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/data-mask" />
+</head>
+
+## Description
+
+The `data-mask` Plugin masks or redacts sensitive fields in request data —
query parameters, headers, and body — before they appear in access logs or
logger plugins (such as `file-logger` or `http-logger`).
+
+This is useful for preventing credentials, tokens, payment card numbers, and
other sensitive information from being written to logs.
+
+The plugin runs in the `log` phase and supports three masking actions:
+
+- `remove`: completely removes the field from the request data.
+- `replace`: replaces the field value with a fixed string.
+- `regex`: applies a regular expression substitution to the field value.
+
+:::note
+
+To have masked query parameters reflected in Nginx access logs, configure
`nginx_config.http.access_log_format` to use the `$request_line` variable
instead of the default `$request`:
+
+```yaml
+nginx_config:
+ http:
+ access_log_format: '"$request_line" $status $body_bytes_sent
"$http_referer" "$http_user_agent"'
+```
+
+:::
+
+## Attributes
+
+| Name | Type | Required | Default | Description
|
+|---------------------|---------|----------|-----------|--------------------------------------------------------------------------------|
+| request | array | False | | List of masking rules
to apply to request data. |
+| max_body_size | integer | False | 1048576 | Maximum request body
size in bytes to process. Bodies larger than this value are skipped for body
masking. |
+| max_req_post_args | integer | False | 100 | Maximum number of
URL-encoded form fields to parse when masking `urlencoded` body data. |
+
+Each object in the `request` array has the following fields:
+
+| Name | Type | Required |
Description
|
+|---------------|--------|-------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
+| type | string | True | Type of
request data to mask. One of `query`, `header`, or `body`.
|
+| name | string | True | Field
name to mask. For `query` and `header` types, this is the parameter or header
name. For `body` type with `body_format: json`, this is a
[JSONPath](https://goessner.net/articles/JsonPath/) expression. |
+| action | string | True | Masking
action to apply. One of `remove`, `replace`, or `regex`.
|
+| body_format | string | Required when `type` is `body` | Format
of the request body. One of `json` or `urlencoded`.
|
+| regex | string | Required when `action` is `regex` | Regular
expression pattern to match against the field value. Capture groups can be
referenced in `value` as `$1`, `$2`, etc. |
+| value | string | Required when `action` is `replace` or `regex` |
Replacement value. When used with `action: regex`, capture groups from `regex`
can be referenced as `$1`, `$2`, etc. |
+
+## Examples
+
+:::note
+You can fetch the `admin_key` from `config.yaml` and save to an environment
variable with the following command:
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### Mask query parameters
+
+The following example creates a route with `data-mask` configured to mask
query parameters. The `password` parameter is removed entirely, `token` is
replaced with a fixed string, and the `card` number is partially masked using a
regex.
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "type": "query",
+ "name": "password",
+ "action": "remove"
+ },
+ {
+ "type": "query",
+ "name": "token",
+ "action": "replace",
+ "value": "*****"
+ },
+ {
+ "type": "query",
+ "name": "card",
+ "action": "regex",
+ "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "logs/access.log"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
+
+Send a request with sensitive query parameters:
+
+```shell
+curl
"http://127.0.0.1:9080/anything?password=secret&token=mytoken&card=1234-5678-9012-3456"
+```
+
+In `logs/access.log`, the logged request URI will have the sensitive fields
masked:
+
+```
+/anything?token=*****&card=1234-****-****-3456
+```
+
+The `password` parameter is absent, `token` is replaced with `*****`, and only
the first and last four digits of the card number are preserved.
+
+### Mask request headers
+
+The following example masks sensitive request headers. The `Authorization`
header is removed, `X-API-Key` is replaced with a fixed string, and a custom
`X-Card-Number` header is partially masked using a regex.
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "type": "header",
+ "name": "Authorization",
+ "action": "remove"
+ },
+ {
+ "type": "header",
+ "name": "X-API-Key",
+ "action": "replace",
+ "value": "[REDACTED]"
+ },
+ {
+ "type": "header",
+ "name": "X-Card-Number",
+ "action": "regex",
+ "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "logs/access.log"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
+
+Send a request with sensitive headers:
+
+```shell
+curl "http://127.0.0.1:9080/anything" \
+ -H "Authorization: Bearer secret-token" \
+ -H "X-API-Key: my-api-key" \
+ -H "X-Card-Number: 1234-5678-9012-3456"
+```
+
+In `logs/access.log`, the logged request headers will have the sensitive
values masked.
+
+### Mask JSON body fields using JSONPath
+
+The following example masks fields in a JSON request body. It removes the
top-level `password` field, replaces the `token` field of every element in the
`users` array, and applies a regex to the nested `credit.card` field of each
user.
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "type": "body",
+ "body_format": "json",
+ "name": "$.password",
+ "action": "remove"
+ },
+ {
+ "type": "body",
+ "body_format": "json",
+ "name": "$.users[*].token",
+ "action": "replace",
+ "value": "*****"
+ },
+ {
+ "type": "body",
+ "body_format": "json",
+ "name": "$.users[*].credit.card",
+ "action": "regex",
+ "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "include_req_body": true,
+ "path": "logs/access.log"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
+
+Send a request with a JSON body containing sensitive fields:
+
+```shell
+curl "http://127.0.0.1:9080/anything" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "password": "secret",
+ "users": [
+ {
+ "name": "alice",
+ "token": "tok_abc123",
+ "credit": { "card": "1234-5678-9012-3456" }
+ },
+ {
+ "name": "bob",
+ "token": "tok_xyz789",
+ "credit": { "card": "9876-5432-1098-7654" }
+ }
+ ]
+ }'
+```
+
+In `logs/access.log`, the logged request body will have the sensitive fields
masked:
+
+```json
+{
+ "users": [
+ {
+ "name": "alice",
+ "token": "*****",
+ "credit": { "card": "1234-****-****-3456" }
+ },
+ {
+ "name": "bob",
+ "token": "*****",
+ "credit": { "card": "9876-****-****-7654" }
+ }
+ ]
+}
+```
+
+The `password` field is absent, all `token` fields are replaced with `*****`,
and card numbers are partially masked.
+
+## Delete Plugin
+
+To remove the `data-mask` Plugin, delete the corresponding JSON configuration
from the Plugin configuration. APISIX will automatically reload and you do not
have to restart for this to take effect.
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {},
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 074c571eb..6499144f7 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -140,7 +140,8 @@
"plugins/csrf",
"plugins/public-api",
"plugins/gm",
- "plugins/chaitin-waf"
+ "plugins/chaitin-waf",
+ "plugins/data-mask"
]
},
{
diff --git a/docs/zh/latest/plugins/data-mask.md
b/docs/zh/latest/plugins/data-mask.md
new file mode 100644
index 000000000..ea7d0abf9
--- /dev/null
+++ b/docs/zh/latest/plugins/data-mask.md
@@ -0,0 +1,309 @@
+---
+title: data-mask
+keywords:
+ - APISIX
+ - API 网关
+ - Plugin
+ - data-mask
+description: API 网关 Apache APISIX data-mask
插件可用于在请求数据写入访问日志或日志插件之前,对敏感字段进行掩码或脱敏处理。
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+<head>
+ <link rel="canonical" href="https://docs.api7.ai/hub/data-mask" />
+</head>
+
+## 描述
+
+`data-mask` 插件可在请求数据(查询参数、请求头、请求体)写入访问日志或日志插件(如
`file-logger`、`http-logger`)之前,对敏感字段进行掩码或脱敏处理。
+
+该插件适用于防止凭证、令牌、支付卡号及其他敏感信息被写入日志的场景。
+
+插件在 `log` 阶段运行,支持以下三种掩码动作:
+
+- `remove`:从请求数据中完全删除该字段。
+- `replace`:将字段值替换为固定字符串。
+- `regex`:对字段值执行正则表达式替换。
+
+:::note
+
+若需要在 Nginx 访问日志中记录掩码后的查询参数,需将 `nginx_config.http.access_log_format` 中的
`$request` 替换为 `$request_line`:
+
+```yaml
+nginx_config:
+ http:
+ access_log_format: '"$request_line" $status $body_bytes_sent
"$http_referer" "$http_user_agent"'
+```
+
+:::
+
+## 属性
+
+| 名称 | 类型 | 必选项 | 默认值 | 描述
|
+|---------------------|---------|--------|-----------|----------------------------------------------------------------------------------|
+| request | array | 否 | | 需应用于请求数据的掩码规则列表。
|
+| max_body_size | integer | 否 | 1048576 |
处理请求体时允许的最大字节数。超过此大小的请求体将跳过请求体掩码处理。 |
+| max_req_post_args | integer | 否 | 100 | 解析 `urlencoded`
请求体时允许的最大表单字段数量。 |
+
+`request` 数组中每个对象包含以下字段:
+
+| 名称 | 类型 | 必选项 | 描述
|
+|---------------|--------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
+| type | string | 是 |
要掩码的请求数据类型。可选值:`query`、`header`、`body`。
|
+| name | string | 是 |
要掩码的字段名称。对于 `query` 和 `header` 类型,填写参数名或请求头名;对于 `body` 类型且 `body_format` 为
`json` 时,填写 [JSONPath](https://goessner.net/articles/JsonPath/) 表达式。 |
+| action | string | 是 |
掩码动作。可选值:`remove`、`replace`、`regex`。
|
+| body_format | string | 当 `type` 为 `body` 时必填 |
请求体格式。可选值:`json`、`urlencoded`。
|
+| regex | string | 当 `action` 为 `regex` 时必填 |
用于匹配字段值的正则表达式。可以在 `value` 中通过 `$1`、`$2` 等方式引用捕获组。
|
+| value | string | 当 `action` 为 `replace` 或 `regex` 时必填 | 替换值。当与
`action: regex` 配合使用时,可通过 `$1`、`$2` 等引用正则捕获组。
|
+
+## 示例
+
+:::note
+
+您可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量:
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed
's/"//g')
+```
+
+:::
+
+### 掩码查询参数
+
+以下示例创建一条路由并配置 `data-mask` 插件对查询参数进行掩码处理:删除 `password` 参数,将 `token`
替换为固定字符串,并通过正则表达式对 `card` 号码进行部分掩码。
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "type": "query",
+ "name": "password",
+ "action": "remove"
+ },
+ {
+ "type": "query",
+ "name": "token",
+ "action": "replace",
+ "value": "*****"
+ },
+ {
+ "type": "query",
+ "name": "card",
+ "action": "regex",
+ "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "logs/access.log"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
+
+发送一个包含敏感查询参数的请求:
+
+```shell
+curl
"http://127.0.0.1:9080/anything?password=secret&token=mytoken&card=1234-5678-9012-3456"
+```
+
+在 `logs/access.log` 中,记录的请求 URI 将对敏感字段进行掩码处理:
+
+```
+/anything?token=*****&card=1234-****-****-3456
+```
+
+`password` 参数已被删除,`token` 被替换为 `*****`,卡号仅保留首尾各四位数字。
+
+### 掩码请求头
+
+以下示例对敏感请求头进行掩码处理:删除 `Authorization` 请求头,将 `X-API-Key` 替换为固定字符串,并通过正则表达式对自定义请求头
`X-Card-Number` 进行部分掩码。
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "type": "header",
+ "name": "Authorization",
+ "action": "remove"
+ },
+ {
+ "type": "header",
+ "name": "X-API-Key",
+ "action": "replace",
+ "value": "[REDACTED]"
+ },
+ {
+ "type": "header",
+ "name": "X-Card-Number",
+ "action": "regex",
+ "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "logs/access.log"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
+
+发送一个包含敏感请求头的请求:
+
+```shell
+curl "http://127.0.0.1:9080/anything" \
+ -H "Authorization: Bearer secret-token" \
+ -H "X-API-Key: my-api-key" \
+ -H "X-Card-Number: 1234-5678-9012-3456"
+```
+
+在 `logs/access.log` 中,记录的请求头将对敏感值进行掩码处理。
+
+### 使用 JSONPath 掩码 JSON 请求体字段
+
+以下示例对 JSON 请求体中的字段进行掩码处理:删除顶层的 `password` 字段,替换 `users` 数组中每个元素的 `token`
字段,并对每个用户的嵌套字段 `credit.card` 应用正则掩码。
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "type": "body",
+ "body_format": "json",
+ "name": "$.password",
+ "action": "remove"
+ },
+ {
+ "type": "body",
+ "body_format": "json",
+ "name": "$.users[*].token",
+ "action": "replace",
+ "value": "*****"
+ },
+ {
+ "type": "body",
+ "body_format": "json",
+ "name": "$.users[*].credit.card",
+ "action": "regex",
+ "regex": "(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "include_req_body": true,
+ "path": "logs/access.log"
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
+
+发送一个包含敏感字段的 JSON 请求体:
+
+```shell
+curl "http://127.0.0.1:9080/anything" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "password": "secret",
+ "users": [
+ {
+ "name": "alice",
+ "token": "tok_abc123",
+ "credit": { "card": "1234-5678-9012-3456" }
+ },
+ {
+ "name": "bob",
+ "token": "tok_xyz789",
+ "credit": { "card": "9876-5432-1098-7654" }
+ }
+ ]
+ }'
+```
+
+在 `logs/access.log` 中,记录的请求体将对敏感字段进行掩码处理:
+
+```json
+{
+ "users": [
+ {
+ "name": "alice",
+ "token": "*****",
+ "credit": { "card": "1234-****-****-3456" }
+ },
+ {
+ "name": "bob",
+ "token": "*****",
+ "credit": { "card": "9876-****-****-7654" }
+ }
+ ]
+}
+```
+
+`password` 字段已被删除,所有 `token` 字段被替换为 `*****`,卡号进行了部分掩码处理。
+
+## 删除插件
+
+当你需要删除该插件时,可以通过如下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X
PUT -d '
+{
+ "uri": "/anything",
+ "plugins": {},
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "httpbin.org:80": 1
+ }
+ }
+}'
+```
diff --git a/t/APISIX.pm b/t/APISIX.pm
index facf13e4d..b3cafb832 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -678,7 +678,7 @@ _EOC_
require("apisix").http_exit_worker()
}
- log_format main escape=default '\$remote_addr - \$remote_user
[\$time_local] \$http_host "\$request" \$status \$body_bytes_sent
\$request_time "\$http_referer" "\$http_user_agent" \$upstream_addr
\$upstream_status \$apisix_upstream_response_time
"\$upstream_scheme://\$upstream_host\$upstream_uri" \$request_llm_model
\$llm_model \$llm_time_to_first_token \$llm_prompt_tokens
\$llm_completion_tokens "\$rate_limiting_info"';
+ log_format main escape=default '\$remote_addr - \$remote_user
[\$time_local] \$http_host "\$request_line" \$status \$body_bytes_sent
\$request_time "\$http_referer" "\$http_user_agent" \$upstream_addr
\$upstream_status \$apisix_upstream_response_time
"\$upstream_scheme://\$upstream_host\$upstream_uri" \$request_llm_model
\$llm_model \$llm_time_to_first_token \$llm_prompt_tokens
\$llm_completion_tokens "\$rate_limiting_info"';
# fake server, only for test
server {
@@ -838,6 +838,7 @@ _EOC_
set \$upstream_scheme 'http';
set \$upstream_host \$http_host;
set \$upstream_uri '';
+ set \$request_line '';
content_by_lua_block {
apisix.http_admin()
@@ -855,6 +856,7 @@ _EOC_
set \$upstream_scheme 'http';
set \$upstream_host \$http_host;
set \$upstream_uri '';
+ set \$request_line '';
set \$ctx_ref '';
set \$upstream_cache_zone off;
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index b9f3846ca..eea7505ca 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -92,6 +92,7 @@ attach-consumer-label
forward-auth
opa
authz-keycloak
+data-mask
proxy-cache
body-transformer
ai-request-rewrite
diff --git a/t/plugin/data-mask.t b/t/plugin/data-mask.t
new file mode 100644
index 000000000..d00aae131
--- /dev/null
+++ b/t/plugin/data-mask.t
@@ -0,0 +1,722 @@
+#
+# 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';
+
+no_long_string();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (! $block->request) {
+ $block->set_value("request", "GET /t");
+ if (!$block->response_body) {
+ $block->set_value("response_body", "passed\n");
+ }
+ }
+});
+
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: mask query
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "remove",
+ "name": "password",
+ "type": "query"
+ },
+ {
+ "action": "replace",
+ "name": "token",
+ "type": "query",
+ "value": "*****"
+ },
+ {
+ "action": "regex",
+ "name": "card",
+ "regex":
"(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "type": "query",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "mask-query.log.1"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 2: verify
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local code =
t("/hello?password=abc&token=xyz&card=1234-1234-1234-1234", ngx.HTTP_GET)
+ local fd, err = io.open("mask-query.log.1", "r")
+ if not fd then
+ core.log.error("failed to open file: ", err)
+ return
+ end
+ local line = fd:read()
+ local log = core.json.decode(line)
+
+ if log.request.querystring.password then
+ ngx.say("password arg mask failed: " ..
log.request.querystring.password)
+ return
+ end
+ if log.request.querystring.token ~= "*****" then
+ ngx.say("token arg mask failed: " ..
log.request.querystring.token)
+ return
+ end
+ if log.request.querystring.card ~= "1234-****-****-1234" then
+ ngx.say("card arg mask failed: " ..
log.request.querystring.card)
+ return
+ end
+ if log.request.uri ~=
"/hello?token=*****&card=1234-****-****-1234" and
+ log.request.uri ~=
"/hello?card=1234-****-****-1234&token=*****" then
+ ngx.say("uri mask failed: " .. log.request.uri)
+ return
+ end
+
+ os.remove("mask-query.log.1")
+ ngx.say("success")
+ }
+ }
+--- response_body
+success
+
+
+
+=== TEST 3: mask header
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "remove",
+ "name": "password",
+ "type": "header"
+ },
+ {
+ "action": "replace",
+ "name": "token",
+ "type": "header",
+ "value": "*****"
+ },
+ {
+ "action": "regex",
+ "name": "card",
+ "regex":
"(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "type": "header",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "mask-header.log.2"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 4: verify
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local headers = {}
+ headers["password"] = "abc"
+ headers["token"] = "xyz"
+ headers["card"] = "1234-1234-1234-1234"
+ local code = t("/hello", ngx.HTTP_GET, "", nil, headers)
+
+ local fd, err = io.open("mask-header.log.2", "r")
+ if not fd then
+ core.log.error("failed to open file: ", err)
+ return
+ end
+ local line = fd:read()
+ local log = core.json.decode(line)
+
+ if log.request.headers.password then
+ ngx.say("password header mask failed: " ..
log.request.headers.password)
+ return
+ end
+ if log.request.headers.token ~= "*****" then
+ ngx.say("token header mask failed: " ..
log.request.headers.token)
+ return
+ end
+ if log.request.headers.card ~= "1234-****-****-1234" then
+ ngx.say("card header mask failed: " ..
log.request.headers.card)
+ return
+ end
+
+ os.remove("mask-header.log.2")
+ ngx.say("success")
+ }
+ }
+--- response_body
+success
+
+
+
+=== TEST 5: mask urlencoded body
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "remove",
+ "body_format": "urlencoded",
+ "name": "password",
+ "type": "body"
+ },
+ {
+ "action": "replace",
+ "body_format": "urlencoded",
+ "name": "token",
+ "type": "body",
+ "value": "*****"
+ },
+ {
+ "action": "regex",
+ "body_format": "urlencoded",
+ "name": "card",
+ "regex":
"(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "type": "body",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "include_req_body": true,
+ "path": "mask-urlencoded-body.log"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 6: verify
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local code = t("/hello", ngx.HTTP_POST,
"password=abc&token=xyz&card=1234-1234-1234-1234")
+
+ local fd, err = io.open("mask-urlencoded-body.log", "r")
+ if not fd then
+ core.log.error("failed to open file: ", err)
+ return
+ end
+ local line = fd:read()
+ local log = core.json.decode(line)
+
+ if log.request.body ~= "token=*****&card=1234-****-****-1234" and
+ log.request.body ~= "card=1234-****-****-1234&token=*****" then
+ ngx.say("urlencoded body mask failed: " .. log.request.body)
+ return
+ end
+
+ os.remove("mask-urlencoded-body.log")
+ ngx.say("success")
+ }
+ }
+--- response_body
+success
+
+
+
+=== TEST 7: mask json body
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "remove",
+ "body_format": "json",
+ "name": "$.password",
+ "type": "body"
+ },
+ {
+ "action": "replace",
+ "body_format": "json",
+ "name": "users[*].token",
+ "type": "body",
+ "value": "*****"
+ },
+ {
+ "action": "regex",
+ "body_format": "json",
+ "name": "$.users[*].credit.card",
+ "regex":
"(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "type": "body",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "include_req_body": true,
+ "path": "mask-json-body.log"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 8: verify
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local code = t("/hello",
+ ngx.HTTP_POST,
+ [[{
+ "password": "abc",
+ "users": [
+ {
+ "token": "xyz",
+ "credit": {
+ "card": "1234-1234-1234-1234"
+ }
+ },
+ {
+ "token": "xyz",
+ "credit": {
+ "card": "1234-1234-1234-1234"
+ }
+ }
+ ]
+ }]]
+ )
+
+ local fd, err = io.open("mask-json-body.log", "r")
+ if not fd then
+ core.log.error("failed to open file: ", err)
+ return
+ end
+ local line = fd:read()
+ local log = core.json.decode(line)
+
+ local body = core.json.decode(log.request.body)
+ if body.password then
+ ngx.say("$.password mask failed: " .. body.password)
+ return
+ end
+ for _, user in ipairs(body.users) do
+ if user.token ~= "*****" then
+ ngx.say("$.users[*].token mask failed: " .. user.token)
+ return
+ end
+ if user.credit.card ~= "1234-****-****-1234" then
+ ngx.say("$.users[*].credit.card mask failed: " ..
user.credit.card)
+ return
+ end
+ end
+
+ os.remove("mask-json-body.log")
+ ngx.say("success")
+ }
+ }
+--- response_body
+success
+
+
+
+=== TEST 9: plugin within global rule should not throw error for missing body.
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/global_rules/1',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "remove",
+ "name": "password",
+ "type": "query"
+ },
+ {
+ "action": "replace",
+ "name": "token",
+ "type": "query",
+ "value": "*****"
+ },
+ {
+ "action": "regex",
+ "name": "card",
+ "regex":
"(\\d+)\\-\\d+\\-\\d+\\-(\\d+)",
+ "type": "query",
+ "value": "$1-****-****-$2"
+ }
+ ]
+ },
+ "file-logger": {
+ "path": "mask-query.log.4"
+ }
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 10: verify
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local code = t("/random", ngx.HTTP_POST,
"password=abc&token=xyz&card=1234-1234-1234-1234")
+
+ ngx.say("code: ", code)
+ }
+ }
+--- response_body
+code: 404
+--- no_error_log
+no request body found
+
+
+
+=== TEST 11: create plugin with default value for `max_req_post_args`
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "regex",
+ "body_format": "urlencoded",
+ "name": "arg100",
+ "regex": "(\\d+)$",
+ "type": "body",
+ "value": "$1"
+ }
+ ]
+ },
+ "file-logger": {
+ "include_req_body": true,
+ "path": "mask-urlencoded-body.log"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 12: verify default value for `max_req_post_args`
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local url_encoded = "arg1=1"
+ for i = 2, 110, 1 do
+ url_encoded = url_encoded .. "&arg" .. i .. "=" .. i
+ end
+
+ local code = t("/hello", ngx.HTTP_POST, url_encoded)
+
+ local fd, err = io.open("mask-urlencoded-body.log", "r")
+ if not fd then
+ core.log.error("failed to open file: ", err)
+ return
+ end
+ local line = fd:read()
+ local log = core.json.decode(line)
+ local match100, err = ngx.re.match(log.request.body, "arg100=100")
+ local match101, err = ngx.re.match(log.request.body, "arg101=101")
+ os.remove("mask-urlencoded-body.log")
+ if match100 and not match101 then
+ ngx.say("success")
+ return
+ end
+ ngx.say("failed: match100=" .. tostring(match100) .. ", match101="
.. tostring(match101))
+ }
+ }
+--- response_body
+success
+
+
+
+=== TEST 13: create plugin with custom `max_req_post_args` value
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "regex",
+ "body_format": "urlencoded",
+ "name": "arg10",
+ "regex": "(\\d+)$",
+ "type": "body",
+ "value": "$1"
+ }
+ ],
+ "max_req_post_args": 10
+ },
+ "file-logger": {
+ "include_req_body": true,
+ "path": "mask-urlencoded-body.log"
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 14: verify number of args
+--- config
+ location /t {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local t = require("lib.test_admin").test
+
+ local url_encoded = "arg1=1"
+ for i = 2, 110, 1 do
+ url_encoded = url_encoded .. "&arg" .. i .. "=" .. i
+ end
+
+ local code = t("/hello", ngx.HTTP_POST, url_encoded)
+
+ local fd, err = io.open("mask-urlencoded-body.log", "r")
+ if not fd then
+ core.log.error("failed to open file: ", err)
+ return
+ end
+ local line = fd:read()
+ local log = core.json.decode(line)
+ local match10, err = ngx.re.match(log.request.body, "arg10=10")
+ local match11, err = ngx.re.match(log.request.body, "arg11=11")
+ os.remove("mask-urlencoded-body.log")
+ if match10 and not match11 then
+ ngx.say("success")
+ return
+ end
+ ngx.say("failed: match10=" .. tostring(match10) .. ", match11=" ..
tostring(match11))
+
+ }
+ }
+--- response_body
+success
+
+
+
+=== TEST 15: create route for access log masking test
+--- 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": {
+ "data-mask": {
+ "request": [
+ {
+ "action": "remove",
+ "name": "password",
+ "type": "query"
+ },
+ {
+ "action": "replace",
+ "name": "token",
+ "type": "query",
+ "value": "*****"
+ }
+ ]
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1982": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/hello"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 16: verify access log masks sensitive query parameters
+--- extra_yaml_config
+nginx_config:
+ http:
+ access_log_format: main '$request_line';
+--- request
+GET /hello?password=secret&token=mytoken
+--- access_log eval
+qr/GET \/hello\?token=\*\*\*\*\* HTTP\/\d+\.\d+/