NGINX:混淆 access_log 中的密码
NGINX: Obfuscate password in access_log
我想在访问日志中记录 $request_body
。
但是有些请求有一些 JSON 字段是敏感的,比如密码。
示例:
[2019-03-28] 201 - POST /api/user/add HTTP/1.1 - {\x22email\x22:\x22test@test.com\x22,\x22password\x22:\x22myPassword\x22}
有没有办法混淆密码值,使输出看起来像这样:
[2019-03-28] 201 - POST /api/user/add HTTP/1.1 - {\x22email\x22:\x22test@test.com\x22,\x22password\x22:\x22****\x22}
查看此博客,其中讨论了为日志屏蔽用户数据:https://www.nginx.com/blog/data-masking-user-privacy-nginscript/
这里有一些正则表达式模式,可用于混淆请求 body 各种格式的数据。
当然,您需要做的第一件事是使用 log_format
指令将混淆数据添加到日志文件行格式:
log_format custom '$remote_addr - $remote_user [$time_local] '
'"$request" "$obfuscated_request_body" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
我们来看下面postbody种数据格式(假设我们需要混淆的字段是password
)
- 请求body是一个JSON字符串(典型的RESTAPI请求)
JSON样本:
{"email":"test@test.com","password":"myPassword"}
转义 JSON 字符串:
{\x22email\x22:\x22test@test.com\x22,\x22password\x22:\x22myPassword\x22}
nginx map
块:
map $request_body $obfuscated_request_body {
"~(.*[{,]\x22password\x22:\x22).*?(\x22[,}].*)" ********;
default $request_body;
}
- 请求 body 是
name
和 value
对的 JSON 数组(由 jQuery serializeArray()
返回函数)
JSON样本:
[{"name":"email","value":"test@test.com"},{"name":"password","value":"myPassword"}]
转义 JSON 字符串:
[{\x22name\x22:\x22email\x22,\x22value\x22:\x22test@test.com\x22},{\x22name\x22:\x22password\x22,\x22value\x22:\x22myPassword\x22}]
nginx map
块:
map $request_body $obfuscated_request_body {
"~(.*[\[,]{\x22name\x22:\x22password\x22,\x22value\x22:\x22).*?(\x22}[,\]].*)" ********;
default $request_body;
}
- 请求body是一个urlencoded字符串(由HTML表单提交
enctype="application/x-www-form-urlencoded"
)
POST body样本:
login=test%40test.com&password=myPassword
nginx map
块:
nginx map
块:
map $request_body $obfuscated_request_body {
~(^|.*&)(password=)[^&]*(&.*|$) ********;
default $request_body;
}
如果您需要混淆多个数据字段,您可以链接多个map
转换:
log_format custom '$remote_addr - $remote_user [$time_local] '
'"$request" "$obfuscated_request_body_2" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
map $request_body $obfuscated_request_body_1 {
"~(.*[{,]\x22password\x22:\x22).*?(\x22[,}].*)" ********;
default $request_body;
}
map $obfuscated_request_body_1 $obfuscated_request_body_2 {
"~(.*[{,]\x22email\x22:\x22).*?(\x22[,}].*)" ********;
default $request_body;
}
所有给定的正则表达式只能与 log_format
nginx 指令的 escape=default
转义模式 一起使用!如果出于某种原因你需要将此模式更改为 escape=json
(可从 nginx 1.11.8 获得)或 escape=none
(可从 nginx 1.13.10 获得),我也为这种转义模式构建了正则表达式,但对于在指定 pcre_jit on;
指令之前,一些奇怪的原因无法使它们与 nginx 一起工作(尽管它们通过了其他 PCRE 测试)。对于那些感兴趣的人,这些正则表达式是
- 对于
escape=json
转义模式:
map $request_body $obfuscated_request_body {
"~(.*[{,]\\"password\\":\\")(?:[^\]|\{3}\"|\{2}[bfnrt]|\{4})*(\\"[,}].*)" ********;
default $request_body;
}
用于 JSON 字符串,并且
map $request_body $obfuscated_request_body {
"~(.*[\[,]{\\"name\\":\\"password\\",\\"value\\":\\")(?:[^\]|\{3}\"|\{2}[bfnrt]|\{4})*(\\"}[,\]].*)" ********;
default $request_body;
}
对于 JSON 个 name
和 value
对数组。
- 对于
escape=none
转义模式:
map $request_body $obfuscated_request_body {
"~(.*[{,]\"password\":\")(?:[^\\"]|\.)*(\"[,}].*)' ********;
default $request_body;
}
用于 JSON 字符串,并且
map $request_body $obfuscated_request_body {
"~(.*[\[,]{\"name\":\"password\",\"value\":\")(?:[^\\"]|\.)*(\"}[,\]].*)" ********;
default $request_body;
}
对于 JSON 个 name
和 value
对数组。
奖励 - 混淆 GET 请求查询参数
有时人们还需要混淆作为 GET 请求查询参数传递的数据。为了做到这一点,同时保留原有的nginx访问日志格式,我们先来看看默认的访问日志格式:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
nginx bulit-in $request
变量可以表示为$request_method $request_uri $server_protocol
变量序列:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request_method $request_uri $server_protocol" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
我们需要混淆部分$request_uri
变量数据:
log_format custom '$remote_addr - $remote_user [$time_local] '
'"$request_method $obfuscated_request_uri $server_protocol" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
map $request_uri $obfuscated_request_uri {
~(.+\?)(.*&)?(password=)[^&]*(&.*|$) ********;
default $request_uri;
}
要混淆多个查询参数,您可以链接多个 map
翻译,如上所示。
更新 - 安全注意事项
Alvin Thompson 评论了 OP 的问题,提到了一些攻击媒介,例如非常大的压缩请求。
值得一提的是,nginx 会以压缩形式记录这些请求 "as-is",因此日志文件不会以不可预测的方式增长。
假设我们的日志文件格式如下:
log_format debug '$remote_addr - $remote_user [$time_local] '
'"$request" $request_length $content_length '
'"$request_body" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
使用 gzipped body 的 5,000 个空格的请求将被记录为
127.0.0.1 - - [09/Feb/2020:05:27:41 +0200] "POST /dump.php HTTP/1.1" 193 41 "\x1F\x8B\x08\x00\x00\x00\x00\x00\x00\x0B\xED\xC11\x01\x00\x00\x00\xC2\xA0*\xEB\x9F\xD2\x14~@\x01\x00\x00\x00\x00o\x03`,\x0B\x87\x88\x13\x00\x00" 200 6881 "-" "curl/7.62.0"
如您所见,$request_length
和 $content_length
值(193 和 41)反映了来自客户端的传入数据的长度,不是解压缩数据流的字节数。
为了过滤异常大的未压缩请求,您还可以按长度过滤请求主体:
map $content_length $processed_request_body {
# Here are some regexes for log filtering by POST body maximum size
# (only one should be used at a time)
# Content length value is 4 digits or more ($request_length > 999)
"~(.*\d{4})" "Too big (request length bytes)";
# Content length > 499
"~^((?:[5-9]|\d{2,})\d{2})" "Too big (request length bytes)";
# Content length > 2999
"~^((?:[3-9]|\d{2,})\d{3})" "Too big (request length bytes)";
default $request_body;
}
map $processed_request_body $obfuscated_request_body {
...
default $processed_request_body;
}
我想在访问日志中记录 $request_body
。
但是有些请求有一些 JSON 字段是敏感的,比如密码。
示例:
[2019-03-28] 201 - POST /api/user/add HTTP/1.1 - {\x22email\x22:\x22test@test.com\x22,\x22password\x22:\x22myPassword\x22}
有没有办法混淆密码值,使输出看起来像这样:
[2019-03-28] 201 - POST /api/user/add HTTP/1.1 - {\x22email\x22:\x22test@test.com\x22,\x22password\x22:\x22****\x22}
查看此博客,其中讨论了为日志屏蔽用户数据:https://www.nginx.com/blog/data-masking-user-privacy-nginscript/
这里有一些正则表达式模式,可用于混淆请求 body 各种格式的数据。
当然,您需要做的第一件事是使用 log_format
指令将混淆数据添加到日志文件行格式:
log_format custom '$remote_addr - $remote_user [$time_local] '
'"$request" "$obfuscated_request_body" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
我们来看下面postbody种数据格式(假设我们需要混淆的字段是password
)
- 请求body是一个JSON字符串(典型的RESTAPI请求)
JSON样本:
{"email":"test@test.com","password":"myPassword"}
转义 JSON 字符串:
{\x22email\x22:\x22test@test.com\x22,\x22password\x22:\x22myPassword\x22}
nginx map
块:
map $request_body $obfuscated_request_body {
"~(.*[{,]\x22password\x22:\x22).*?(\x22[,}].*)" ********;
default $request_body;
}
- 请求 body 是
name
和value
对的 JSON 数组(由 jQueryserializeArray()
返回函数)
JSON样本:
[{"name":"email","value":"test@test.com"},{"name":"password","value":"myPassword"}]
转义 JSON 字符串:
[{\x22name\x22:\x22email\x22,\x22value\x22:\x22test@test.com\x22},{\x22name\x22:\x22password\x22,\x22value\x22:\x22myPassword\x22}]
nginx map
块:
map $request_body $obfuscated_request_body {
"~(.*[\[,]{\x22name\x22:\x22password\x22,\x22value\x22:\x22).*?(\x22}[,\]].*)" ********;
default $request_body;
}
- 请求body是一个urlencoded字符串(由HTML表单提交
enctype="application/x-www-form-urlencoded"
)
POST body样本:
login=test%40test.com&password=myPassword
nginx map
块:
nginx map
块:
map $request_body $obfuscated_request_body {
~(^|.*&)(password=)[^&]*(&.*|$) ********;
default $request_body;
}
如果您需要混淆多个数据字段,您可以链接多个map
转换:
log_format custom '$remote_addr - $remote_user [$time_local] '
'"$request" "$obfuscated_request_body_2" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
map $request_body $obfuscated_request_body_1 {
"~(.*[{,]\x22password\x22:\x22).*?(\x22[,}].*)" ********;
default $request_body;
}
map $obfuscated_request_body_1 $obfuscated_request_body_2 {
"~(.*[{,]\x22email\x22:\x22).*?(\x22[,}].*)" ********;
default $request_body;
}
所有给定的正则表达式只能与 log_format
nginx 指令的 escape=default
转义模式 一起使用!如果出于某种原因你需要将此模式更改为 escape=json
(可从 nginx 1.11.8 获得)或 escape=none
(可从 nginx 1.13.10 获得),我也为这种转义模式构建了正则表达式,但对于在指定 pcre_jit on;
指令之前,一些奇怪的原因无法使它们与 nginx 一起工作(尽管它们通过了其他 PCRE 测试)。对于那些感兴趣的人,这些正则表达式是
- 对于
escape=json
转义模式:
map $request_body $obfuscated_request_body {
"~(.*[{,]\\"password\\":\\")(?:[^\]|\{3}\"|\{2}[bfnrt]|\{4})*(\\"[,}].*)" ********;
default $request_body;
}
用于 JSON 字符串,并且
map $request_body $obfuscated_request_body {
"~(.*[\[,]{\\"name\\":\\"password\\",\\"value\\":\\")(?:[^\]|\{3}\"|\{2}[bfnrt]|\{4})*(\\"}[,\]].*)" ********;
default $request_body;
}
对于 JSON 个 name
和 value
对数组。
- 对于
escape=none
转义模式:
map $request_body $obfuscated_request_body {
"~(.*[{,]\"password\":\")(?:[^\\"]|\.)*(\"[,}].*)' ********;
default $request_body;
}
用于 JSON 字符串,并且
map $request_body $obfuscated_request_body {
"~(.*[\[,]{\"name\":\"password\",\"value\":\")(?:[^\\"]|\.)*(\"}[,\]].*)" ********;
default $request_body;
}
对于 JSON 个 name
和 value
对数组。
奖励 - 混淆 GET 请求查询参数
有时人们还需要混淆作为 GET 请求查询参数传递的数据。为了做到这一点,同时保留原有的nginx访问日志格式,我们先来看看默认的访问日志格式:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
nginx bulit-in $request
变量可以表示为$request_method $request_uri $server_protocol
变量序列:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request_method $request_uri $server_protocol" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
我们需要混淆部分$request_uri
变量数据:
log_format custom '$remote_addr - $remote_user [$time_local] '
'"$request_method $obfuscated_request_uri $server_protocol" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
map $request_uri $obfuscated_request_uri {
~(.+\?)(.*&)?(password=)[^&]*(&.*|$) ********;
default $request_uri;
}
要混淆多个查询参数,您可以链接多个 map
翻译,如上所示。
更新 - 安全注意事项
Alvin Thompson 评论了 OP 的问题,提到了一些攻击媒介,例如非常大的压缩请求。 值得一提的是,nginx 会以压缩形式记录这些请求 "as-is",因此日志文件不会以不可预测的方式增长。
假设我们的日志文件格式如下:
log_format debug '$remote_addr - $remote_user [$time_local] '
'"$request" $request_length $content_length '
'"$request_body" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
使用 gzipped body 的 5,000 个空格的请求将被记录为
127.0.0.1 - - [09/Feb/2020:05:27:41 +0200] "POST /dump.php HTTP/1.1" 193 41 "\x1F\x8B\x08\x00\x00\x00\x00\x00\x00\x0B\xED\xC11\x01\x00\x00\x00\xC2\xA0*\xEB\x9F\xD2\x14~@\x01\x00\x00\x00\x00o\x03`,\x0B\x87\x88\x13\x00\x00" 200 6881 "-" "curl/7.62.0"
如您所见,$request_length
和 $content_length
值(193 和 41)反映了来自客户端的传入数据的长度,不是解压缩数据流的字节数。
为了过滤异常大的未压缩请求,您还可以按长度过滤请求主体:
map $content_length $processed_request_body {
# Here are some regexes for log filtering by POST body maximum size
# (only one should be used at a time)
# Content length value is 4 digits or more ($request_length > 999)
"~(.*\d{4})" "Too big (request length bytes)";
# Content length > 499
"~^((?:[5-9]|\d{2,})\d{2})" "Too big (request length bytes)";
# Content length > 2999
"~^((?:[3-9]|\d{2,})\d{3})" "Too big (request length bytes)";
default $request_body;
}
map $processed_request_body $obfuscated_request_body {
...
default $processed_request_body;
}