Алгоритм выбора location обязателен к знанию при настройке nginx. Тем не менее, на официальном сайте nginx (на 2018 год) не сказано ни слова про алгоритм выбора в случаях, когда какие-то location'ы вложены друг в друга, а в статьях в интернете приводятся в корне неверные алгоритмы. В статье также будет дан пример уязвимого конфига.

Частный случай с одним уровнем вложенности


Если Вы новичок в nginx, то следует рассмотреть вначале частный случай без использования вложненных location, т.?к. алгоритм для частного случая значительно проще:

  1. Вначале будет искаться равенство (=). Оно имеет высший приоритет.
  2. Потом будет искаться максимальный по длине префиксный location (( ? ) или (^~)), после чего будет проверено, есть ли на найденном location модификатор приоритета (^~), и если он есть, то будет возвращён этот location.
  3. Потом будут проверяться регулярные выражения ((~) и (~*)) сверху вниз. При совпадении будет возвращён первый location из них.
  4. Потом вернётся тот префиксный location, который мы нашли до этого.

Обратите внимание, что этот алгоритм не применим при наличии вложенных location.

Общий случай с вложенными location


  1. Стартуем с верхнего уровня.
  2. Если на текущем уровне выполняется равенство (=), поиск прекращается — это и будет результат, т.?к. такой location не может иметь никаких других вложенных location.
  3. В противном случае ищем на текущем уровне самый большой префиксный location (( ? ) или (^~)).
    • Если такой префиксный location существует, то делаем его текущим уровнем и переходим к п.?2.
    • В противном случае выходим из цикла.
  4. Мы вышли из цикла. На данный момент мы нашли «самый большой» префиксный location, но не думайте, что это самый большой из всех. Пример:

    location /abc {
    	location /abcdefghi {
    		…
    	}
    }
    
    location /abcdef {
    	…
    }
    

    В данном примере мы перейдём в /abcdef, т.?к. на его уровне он переборол более короткий /abc. Но по факту существуют location и больше него.
  5. Теперь в найденном location мы ищем первый верный regexp. При нахождении поиск полностью прекращается. Обратите внимание: в этом пункте мы по факту ищем regexp на самом нижнем уровне, а не на верхнем, как многие могли бы подумать. Т.?е. поиск regexp идёт снизу, а не сверху (но внутри одного уровня идёт сверху, а не снизу).
    • Далее, если ничего не найдено, поднимаемся на один уровень вверх и аналогично ищем первый regexp, но в этот раз уже только при условии, что location, в котором мы были до этого, не имел метки (^~). Повторяем этот пункт до тех пор, пока подниматься будет некуда.
    • При этом нужно иметь ввиду:
      • Даже если какой-то из уровней имеет метку (^~), это не значит, что мы не осуществляем подъём. Подъём осуществляется всегда, но если более нижний уровень имел метку (^~), то на текущем уровне поиск regexp'ов не проводится.
      • Возможности запретить проверку regexp в самом нижнем уровне нет — для этого нужно создать ещё один вложенный уровень. А вот запретить проверку regexp на нулевом уровне можно — для этого location первого уровня (который находится на нулевом уровне) должен иметь метку (^~).
  6. Мы сделали подъём по дереву, но так и не нашли ни одного 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


  1. Если try_files не содержит кода ошибки последним параметром, то будет сделано перенаправление в другой location, т.?к. последний параметр всегда делает перенаправление. Обратите внимание: код ошибки в try_files должен писаться через равно (=).
  2. index и error_page при срабатывании всегда делают перенаправление в другой location. Также перенаправление делает rewrite, если добавить в него флаг last.

Другое


  1. При выборе location не учитывается строка запроса, которая начинается со знака "?".

Отказ от ответственности


Алгоритм в статье составлен мной на основе моих личных наблюдений, и не факт, что он является правильным. К сожалению, официальной документации нет, а исходники я не читал. Если кто-то найдёт ошибки в алгоритме, просьба написать личным сообщением или в комментариях.

Комментарии (20)


  1. romy4
    03.02.2018 14:04

    почему же тогда пришлось переместить этот кусок
    location ~ \.html$ {
    try_files $uri =404;
    }

    над этим
    location ~ / {
    rewrite . /index.php last;
    }

    иначе всегда отрабатывал location ~ / при запросе к *.html файлам


    1. vitaliy2 Автор
      03.02.2018 14:05

      Перечитайте статью. У Вас один уровень вложенности. В пределах одного уровня вложенности поиск регулярных выражений происходит сверху вниз.

      Цитата из статьи:

      (но внутри одного уровня идёт сверху, а не снизу).

      Но в пределах нескольких уровней мы вначале находим префиксный location, а потом делаем обратный подъём по дереву.


      1. romy4
        03.02.2018 14:16

        у меня как-то не сошлось понимание написанного:

        1. Стартуем с верхнего уровня.
        2.…
        3. В противном случае ищем на текущем уровне самый большой префиксный location (( ? ) или (^~)).

        «текущим» для этого простого конфига является самый верхний уровень. Тогда почему игнорируется пункт №3?


        1. vitaliy2 Автор
          03.02.2018 14:21

          Всё верно, на первой итерации текущий уровень — это верхний. Но ведь в третьем пункте сказано:

          Если такой префиксный location существует, то делаем его текущим уровнем и переходим к п.?2.

          В общем, я не понимаю Ваш вопрос. Возможно, Вам стоит перечитать алгоритм. Либо мне попытаться объяснить его более просто, но способов упрощения я пока не знаю.

          Тогда почему игнорируется пункт №3?
          Где он игнорируется?


          1. romy4
            03.02.2018 14:33

            Если уровней всего один, то какой location будет выбран первым? Вот это, если можно, хочется услышать дополнительно в алгоритме.


            1. vitaliy2 Автор
              03.02.2018 14:39

              Если уровней один, то всё слишком просто.

              1. Вначале будет искаться равенство
              2. Потом будет искаться максимальный префиксный location, после чего будет проверено, есть ли на нём модификатор приоритета
              3. Потом regexp сверху вниз
              4. Потом вернётся тот префиксный location, который мы нашли до этого

              Я сейчас добавлю это в статью, т.?к. не все пишут конфиги с вложенными location, а без них алгоритм можно описать значительно проще. Просто я думал, что это уже итак все знают, и интересен был более общий случай.


            1. vitaliy2 Автор
              03.02.2018 14:45

              Добавил частный случай с одним уровнем вложенности.


              1. romy4
                03.02.2018 14:47

                Это привлечёт внимание тех, кто ищет с одним уровнем вложенности. Заодно разберутся и с тем, когда несколько уровней.


                1. vitaliy2 Автор
                  03.02.2018 14:56

                  Спасибо, думаю, Вы правы. Вначале поставил описание частного случая, а потом уже описание общего случая с вложенными location. Многим вложенные уровни вообще могут быть не нужны, и они сюда пришли за простым алгоритмом, а в итоге получили супер-навороченный алгоритм, в котором нереально разобраться.


    1. vitaliy2 Автор
      03.02.2018 14:12

      Ну и на всякий случай скопирую пример из статьи, чтобы было понятнее.

      location ~ \.php$ {
      	…
      }
      
      location /posts/ {
      	location ~ (.*)_2x(\.[a-z]+)$ {
      		try_files $uri $1$2 =404;
      	}
      }
      

      Здесь при запросе /posts/*** в случаях, когда выражение подходит и под первый regexp-location, и под второй regexp-location, приоритет будет отдан второму regexp-location'у, даже несмотря на то, что он идёт после первого.


      1. telkar
        03.02.2018 17:05

        Потому что второй локейшен не regexp-location, а префиксный.
        Документация гласит:

        location можно задать префиксной строкой или регулярным выражением. Регулярные выражения задаются либо с модификатором “~*” (для поиска совпадения без учёта регистра символов), либо с модификатором “~” (с учётом регистра). Чтобы найти location, соответствующий запросу, вначале проверяются location’ы, заданные префиксными строками (префиксные location’ы).


        1. vitaliy2 Автор
          03.02.2018 17:08

          Я имел ввиду второй regexp (среди location'ом он третий по счёту).

          А насчёт документации — там не говорится ничего чётко. Вот цитата, которую Вы привели:

          Чтобы найти location, соответствующий запросу, вначале проверяются location’ы, заданные префиксными строками

          Ну вот нашли мы префиксный location, а что дальше? И имелся ли ввиду поиск префиксного location на текущем уровне или сразу на всех? На эти два вопроса и даёт ответ данная статья. А вопросы то очень важные.


  1. symbix
    03.02.2018 15:02

    К сожалению, официальной документации нельзя доверять

    Почему? Что там не так?


    1. vitaliy2 Автор
      03.02.2018 15:12
      -1

      Ну как минимум там даже не потрудились привести алгоритм выбора location при наличии вложенных location. А то, что приведено, не сказано, что это для частного случая, когда вложенных location нет. Нет, там и не сказано, что это для общего случая, но блин… :) Это немного ввод в заблуждение.


      1. symbix
        03.02.2018 17:22
        +1

        Черт его знает, мне все понятно, но я пользовался вложенными location-ами еще когда они были задокументированы только на языке С, так что мне сложно судить, насколько оно может быть непонятно. Если знаете, как написать лучше — напишите, патчи на документацию они принимают.


  1. Temtaime
    03.02.2018 16:40

    В пункте 2 лучше объяснить подробнее что такое префиксный location, чтобы избежать путаниц.
    Фактически возвращается самый длинный подходящий не регулярный location, если такого нет — идёт поиск по регулярным сверху вниз.


    1. vitaliy2 Автор
      03.02.2018 17:10
      -1

      Предполагается, что читающий уже знает, что такое префиксный location. Это location ( ? ) или (^~).


  1. tgz
    03.02.2018 17:45
    -2

    Хотели как лучше, а получилось как всегда.
    Раз уже замутили схему, где у тебя половина конфига рассматривается как попало, а вторая половина по расположению в конфиге, то могли бы уж тестирование локейшенов сделать. А то сейчас единственный способ — это запуск в debug режиме, прогон запроса ручками и чтение портянки логов.



  1. homm
    05.02.2018 14:48

    Кучу лет пользуюсь nginx, и только сейчас узнал, что location могут быть вложенными! С одной стороны ни разу не было необходимости в таком, с другой предупрежден — значит вооружен. Спасибо!