Самое страшное зло в Nginx - это if в location. Об этом написано много, в том числе на nginx.com. Процитируем кусочек:
The only 100% safe things which may be done inside if in a location context are:
- return ...;
- rewrite ... last;
Казалось бы, если использовать конструкцию вида
location / {
if ( $condition ) {
return 418;
}
...
}
то ничего страшного не произойдет, однако, при определенном "умении", можно сломать даже то, что должно работать на 100%. Но будет ли виноват в нашей поломке if?
Предыдущие статьи
Предположим, что у нас был веб-сервер принимающий POST-запросы:
server {
root /www/example_com;
listen *:80;
server_name .example.com;
location /index.php {
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
$ cat /www/example_com/index.php
<?php
var_dump($_REQUEST);
$ curl \
> -w "HTTP CODE: %{http_code}\n" \
> -d "key1=value1" \
> -X POST \
> "example.com/index.php" \
>
array(1) {
["key1"]=>
string(6) "value1"
}
HTTP CODE: 200
И в какой-то момент мы захотели отфильтровать запросы с пустыми телом (навеяно вопросом на форуме). Первый порыв мысли приводит конфиг к такому виду:
server {
root /www/example_com;
listen *:80;
server_name .example.com;
location /index.php {
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
if ( $request_body = '' ) {
return 418;
}
}
}
Однако, мысль эта ошибочна, и в такой конфигурации мы всегда (как с отправленным телом так и без) получим 418-й ответ:
$ curl \
> -w "HTTP CODE: %{http_code}\n" \
> -X POST \
> "example.com/index.php" \
>
HTTP CODE: 418
$ curl \
> -w "HTTP CODE: %{http_code}\n" \
> -d "key1=value1" \
> -X POST \
> "example.com/index.php" \
>
HTTP CODE: 418
Как определены фазы в Nginx
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_PRECONTENT_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
В нашем условии используется переменная $request_body, значение которой
появляется в location’ах, обрабатываемых директивами proxy_pass, fastcgi_pass, uwsgi_pass и scgi_pass, когда тело было прочитано в буфер в памяти.
Следовательно, значение переменной устанавливается в фазе NGX_HTTP_CONTENT_PHASE, в то время как, директива if модуля ngx_http_rewrite_module исполняется в фазе NGX_HTTP_REWRITE_PHASE, то есть, на момент проверки условия, переменной $request_body не будет присвоено никакого значения вне зависимости от того пуст body или нет, поэтому при любых запросах ответом будет "418 I'm a teapot".
Таким образом, хоть мы и поломали гарантированно работающий вариант, if в location здесь совершенно не причем. Всему "виной" лишь порядок фаз обработки запроса.
Комментарии (25)
aamonster
03.08.2021 12:27А правильное решение есть? Как-то без него статья не смотрится.
Убедить nginx, что у нас тут fastcgi/proxy, как делают в https://stackoverflow.com/questions/4939382/logging-post-data-from-request-body? Как-то не очень...simpleadmin Автор
03.08.2021 12:42Прототип решения по ссылке на форум вполне хорош.
Нам нужно переместить обработку условия в CONTENT-фазу, njs вполне для этого подходит, применительно к примеру из статьи как-то так:js_import check_body.js; server { root /www/example_com; listen *:80; server_name .example.com; location /index.php { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; js_content check_body.body_empty; } }
function body_empty(r) { if (r.method == "POST" && (typeof(r.requestBody) == "undefined" || r.requestBody === null)) { r.return(418, "Empty\n"); } else { r.return(200, "ret:[" + r.requestBody + "]\n"); } } export default {body_empty};
$ curl -w "HTTP CODE: %{http_code}\n" -d "key1=value1" -X POST "example.com/index.php" \ ret:[key1=value1] HTTP CODE: 200
$ curl -w "HTTP CODE: %{http_code}\n" -X POST "example.com/index.php" Empty HTTP CODE: 418
aamonster
03.08.2021 13:02+1А использовать ноду точно будет дешевле, чем прямо в fastcgi-скрипте проверить?
simpleadmin Автор
03.08.2021 13:37Скажем так - я не думаю, что njs станет узким местом.
Но и делать так я точно не стал бы - сопровождать такое никакого желания. Решение только на случай если "можно использовать только nginx".
Как вариант можно использовать https://github.com/calio/form-input-nginx-module:location /index.php { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; set_form_input_multi $data; if ( $data = '' ) { return 418; } }
Но, опять же, спорное решение и в плане поддержки и в плане производительности.
BasilioCat
03.08.2021 13:38Это не нода, а встроенный в nginx JS-движок, по скорости не должен уступать lua в оpenresty. Однако, если есть строгая необходимость логгировать $request_body без лишних скриптов, то можно его проксировать на себя, в отдельный сервер/порт на 127.0.0.1, в котором уже можно делать return и прочие запретные вещи.
aamonster
03.08.2021 17:57Спасибо.
Давайте рассмотрим два крайних случая:
Один реквест из тысячи с пустым body.
Один реквест из тысячи с непустым body.
Собственно, мне просто интересно для каждого из двух случаев – что обойдётся дешевле: "if" вынесенный в отдельную njs-функцию (т.е. в обоих случаях 1000 раз выполнится js) или "if" в начале обработки FastCGI-запроса (сразу после Accept или что там)? Или это зависит от того, на чём FastCGI сделан?
aamonster
03.08.2021 18:00Ну и третий вариант, как написал @RekGRpth– использовать echo_read_request_body. Насколько это дорогое удовольствие?
simpleadmin Автор
04.08.2021 08:21Использование echo_read_request_body - абсолютно бесполезно, точнее просто вредно. Если пакет поднялся до фазы CONTENT, то для того чтобы выполнить if (значит попасть в фазу REWRITE) у него есть только один выход - выполнить проксирование. Но при использовании директивы proxy_pass переменная $request_body получит значение автоматически, то есть echo_read_request_body просто лишено смысла.
simpleadmin Автор
03.08.2021 20:27+1server { root /www/example_com; listen *:80; server_name .example.com; location /index.php { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }
<?php if(!count($_REQUEST)) http_response_code(418); die();
19691
js_import /scripts/nginx/js/check_body.js; server { root /www/example_com; listen *:80; server_name .example.com; location /index.php { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; js_content check_body.body_empty; #if ( $request_body = '' ) { # return 418; #} } }
12263
server { root /www/example_com; listen *:80; server_name .example.com; location /index.php { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; set_form_input_multi $data; if ( $data = '' ) { return 418; } } }
6009
Время в наносекундах от ngx_http_handler:entry до ngx_http_finalize_request:return
Усредненное для 100000 "пустых" POST-запросов.
Nnnnoooo
03.08.2021 20:32Спасибо!
Хороший пример насколько модуль быстрее njs.
Сейчас под рукой нет опенрести чтобы луа проверить, но сразу после выхода njs, когда тестировал луа был самую малость быстрее. Но то может на моих сценариях.simpleadmin Автор
03.08.2021 22:39Хороший пример насколько модуль быстрее njs.
Скорее пример того насколько быстрее будет завершить обработку в REWRITE-фазе, чем в CONTENT
Маршруты пакета для обоих случаев:
$ cat /tmp/tree.txt | grep phase | awk '{print $3}' | uniq ngx_http_core_generic_phase ngx_http_core_rewrite_phase ngx_http_core_find_config_phase ngx_http_core_rewrite_phase $ cat /tmp/tree_njs.txt | grep phase | awk '{print $3}' | uniq ngx_http_core_generic_phase ngx_http_core_rewrite_phase ngx_http_core_find_config_phase ngx_http_core_rewrite_phase ngx_http_core_post_rewrite_phase ngx_http_core_generic_phase ngx_http_core_access_phase ngx_http_core_post_access_phase ngx_http_core_generic_phase ngx_http_core_content_phase
Nnnnoooo
03.08.2021 23:19Спасибо за уточнение.
Надо будет потом потестить все это на реальных данных.
Последнее время забил на все эти защиты на стороне nginx-а, все равно помогают только от самого школьного ддоса.
А если нужна какая-то хитрая обработка запроса именно на стороне nginx-а, то тогда уже известный openresty или в крайнем случае njs
Nnnnoooo
03.08.2021 20:36А насчет fastcgi это идеальные данные, в реальности под нагрузкой будет еще медленнее, т.к. очень плохо параллелится, под сверх нагрузками.
Насколько я понял из задачи это была простая защита от спама пустыми POST запросами и тут бы все в php-fpm все уперлось
Nnnnoooo
03.08.2021 20:29Кстати, проксирование на себя позволяет делать и другие костыли, для обмана порядка фаз обработки запроса.
Но все-таки это вообще не интуитивно и легко в будущем потом сломать если планируются правки конфига. Поэтому раньше было проще опенрести поставить и юзать lua, а сейчас на мейнлайне njs уже можно использовать
RekGRpth
03.08.2021 16:42-2я так обычно делаю, если надо обойти if
location /index.php { if ( $request_body = '' ) { return 418; } try_files /nonexistent @proxy; } location @proxy { fastcgi_pass ... }
главное, чтобы /nonexistent не существовал
simpleadmin Автор
03.08.2021 16:55Как это поможет установить значение в переменной $request_body?
RekGRpth
03.08.2021 17:08-2а вот это уже другой вопрос и на него можно ответить например так echo_read_request_body
simpleadmin Автор
03.08.2021 20:19echo-nginx-module работает в CONTENT-фазе, там мы можем получить $request_body и без сторонних модулей.
Как всё это поможет проверить переменную $request_body в REWRITE-фазе?
RekGRpth
04.08.2021 13:55хм... точно, а вот так сработало
location /index.php { set_form_input $data; if ($request_body = '') { return 418; } try_files /nonexistent @proxy; } location @proxy { fastcgi_pass ... }
simpleadmin Автор
04.08.2021 14:18Этот пример я уже приводил выше.
Так и должно работать, так как директивы модуля работают в фазе REWRITE:$ grep -rnF PHASE ../form-input-nginx-module-master/ ../form-input-nginx-module-master/src/ngx_http_form_input_module.c:372: h = ngx_array_push(&cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers);
simpleadmin Автор
04.08.2021 14:20Ну и try_files здесь не нужен, так как работаем в REWRITE_PHASE, то и if отработает нормально.
DDoSInsurance
04.08.2021 14:50Очень плохой пример.
1) $request_body здесь установился "случайно", просто потому что модуль form-input-nginx-module получил/установил его при парсинге $data.
2) Лишние телодвижения с try_files. Вы поднялись до PRECONTENT_PHASE и зачем-то пошли на второй круг по всем фазам (заново, начиная от REWRITE_PHASE) вместо того чтобы сразу перейти к CONTENT_PHASE. Этот дополнительный проход не выполняет никаких действий вообще.
myc
06.08.2021 23:11+1Я бы проверял не $request_body, а наличие $http_content_length или $http_transfer_encoding = ‘chunked’. Обе эти переменные уже определены на этапе REWRITE_PHASE.
mSnus
Допишите сразу про map вместо if, удобнее будет
simpleadmin Автор
По map есть отличная статья, но а данном случае он также не поможет.