Алгоритм выбора location в Nginx
Алгоритм выбора location обязателен к знанию при настройке nginx. Тем не менее, на официальном сайте nginx (на 2018 год) не сказано ни слова про алгоритм выбора в случаях, когда какие-то location’ы вложены друг в друга, а в статьях в интернете приводятся в корне неверные алгоритмы. Поэтому решил написать свою небольшую заметку.
Если Вы не знали о том, что кроме спуска по дереву вложенных location nginx также делает и подъём по дереву, статья обязательна к прочтению. В статье также будет дан пример уязвимого конфига.
Частный случай с одним уровнем вложенности
Если Вы новичок в nginx, то следует рассмотреть вначале частный случай без использования вложненных location, т. к. алгоритм для частного случая значительно проще:
- Вначале будет искаться равенство. Оно имеет высший приоритет.
- Потом будет искаться максимальный префиксный location, после чего будет проверено, есть ли на нём модификатор приоритета (^~), и если он есть, то будет возвращён этот location.
- Потом будут проверяться регулярные выражения сверху вниз. При первом же совпадении будет возвращён совпавший location.
- Потом вернётся тот префиксный location, который мы нашли до этого.
Обратите внимание, что этот алгоритм неприменим при наличии вложенных location.
Общий случай с вложенными location
- Стартуем с верхнего уровня.
- Если на текущем уровне выполняется равенство (=), поиск прекращается — это и будет результат, т. к. такой location не может иметь никаких других вложенных location.
- В противном случае ищем на текущем уровне самый большой префиксный location (( ) или (^~)).
- Если такой префиксный location существует, то делаем его текущим уровнем и переходим к п. 2.
- В противном случае выходим из цикла.
- Мы вышли из цикла. На данный момент мы нашли «самый большой» префиксный location, но не думайте, что это самый большой из всех. Пример:
location /abc { location /abcdefghi { … } } location /abcdef { … }
В данном примере мы перейдём в /abcdef, т. к. на его уровне он переборол более короткий /abc. Но по факту существуют location и больше него. - Теперь в найденном location мы ищем первый верный regexp. При нахождении поиск полностью прекращается. Обратите внимание: в этом пункте мы по факту ищем regexp на самом нижнем уровне, а не на верхнем, как многие могли бы подумать. Т. е. поиск regexp идёт снизу, а не сверху (но внутри одного уровня идёт сверху, а не снизу).
- Далее, если ничего не найдено, поднимаемся на один уровень вверх и аналогично ищем первый regexp, но в этот раз уже только при условии, что location, в котором мы были до этого, не имел метки (^~). Повторяем этот пункт до тех пор, пока подниматься будет некуда.
- При этом нужно иметь ввиду:
- Даже если какой-то из уровней имеет метку (^~), это не значит, что мы не осуществляем подъём. Подъём осуществляется всегда, но если более нижний уровень имел метку (^~), то на текущем уровне поиск regexp’ов не проводится.
- Возможности запретить проверку regexp в самом нижнем уровне нет — для этого нужно создать ещё один вложенный уровень. А вот запретить проверку regexp на нулевом уровне можно — для этого location первого уровня (который находится на нулевом уровне) должен иметь метку (^~).
- Мы сделали подъём по дереву, но так и не нашли ни одного regexp. Раз regexp не найден, возвращаем «почти самый большой» префиксным location, который был найден ранее. Готово.
Также при этом:
- В версиях 0.7.1–0.8.41 префиксный location ( ) при точном совпадении действует как =
Пример уязвимого конфига
location ~ \.php$ {
deny all; # Здесь должно быть проксирование на php-fpm
}
location /posts/ {
location ~ (.*)_2x(\.[a-z]+)$ {
try_files $uri $1$2 =404;
}
}
В данном конфиге мы настроили игнорирование »_2x», если файл не найден. Например, nginx попробует найти файл /posts/img/a_2x.png как по указанному пути, так и по пути /posts/img/a.png. Но в реальности, если мы запросим /posts/authData_2x.php, то мы получим исходный текст скрипта authData.php в голом виде. Чтобы избежать таких ошибок, нужно знать, как обрабатывается location в nginx.
Также дополнительной защитой может являться хранение скриптов в отдельной директории, недоступной из под обычных location. В этом случае, если наш location на php по каким-то причинам не сработает, пользователь получит ошибку 404, а не исходный текст скрипта.
Перенаправление location
- Если try_files не содержит кода ошибки последним параметром, то будет сделано перенаправление в другой location, т. к. последний параметр всегда делает перенаправление. Обратите внимание: код ошибки в try_files должен писаться через равно (=).
- index и error_page при срабатывании всегда делают перенаправление в другой location. Также перенаправление делает rewrite, если добавить в него флаг last.
Другое
- При выборе location не учитывается строка запроса, которая начинается со знака »?».
Отказ от ответственности
Алгоритм в статье составлен мной на основе моих личных наблюдений, и не факт, что он является правильным. К сожалению, официальной документации нет, а исходники я не читал. Если кто-то найдёт ошибки в алгоритме, просьба написать личным сообщением или в комментариях.