1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Чем опасен if

Коротко

if внутри location заводит неявный вложенный блок: когда условие срабатывает, запрос обслуживает он, а не блок вокруг. Отсюда исчезающие заголовки и proxy_pass, уходящий на другой путь в зависимости от параметра запроса. Безопасны внутри if ровно четыре директивы: return, rewrite, set, break. Всё остальное либо не грузится, либо ведёт себя не так, как написано.

Правило «в if только return и rewrite» знаешь - листай до «Теперь сам». Разборы про исчезнувший заголовок и раздвоившийся прокси.

Блок ставит заголовок безопасности всем ответам, а внутри условия добавляет второй. Что получит клиент по запросу /hdr?x=1?

location /hdr {
    add_header X-Снаружи да always;
    if ($arg_x) { add_header X-Внутри да always; }
    return 200 "ok\n";
}

Ожидание - два заголовка. На стенде приходит один: X-Внутри. Внешний исчез.

На что это похоже

Это как отдать заказ на субподряд. Пока условие ложно, работает твой цех со всеми твоими правилами. Как только условие истинно, работу забирает другой цех - формально твой, стоящий в том же здании, но со своим набором правил. Он унаследует то, чего у него нет своего, и не унаследует ничего из того, где у него есть хоть что-то своё. Заголовки как раз из второй категории: правило наследования у них «всё или ничего», и оно уже встречалось в разделе про язык конфига.

Механизм: неявный вложенный блок

location /hdr add_header X-Снаружи if ($arg_x) add_header X-Внутри return 200 условие ложно: отвечает внешний блок условие истинно: отвечает вложенный
Сработавший if - это второй блок, а не строка внутри первого

Правило

Внутри if можно писать только то, что не относится к обслуживанию запроса: return, rewrite, set и break. Всё остальное - либо ошибка загрузки, либо тихая подмена обработчика.

Разбор: заголовок, который исчез

Три замера одного и того же блока:

запрос заголовки в ответе
/hdr?x=1 (условие истинно) только X-Внутри
/hdr (условие ложно) только X-Снаружи
/hdr-bez (блока if нет вовсе) только X-Снаружи

Первая строка и есть дефект. Пока условие ложно, всё выглядит правильно - поэтому такую конфигурацию выкатывают, посмотрев ответ на обычном запросе. Ломается она только на тех запросах, ради которых её и писали.

Если заголовок нужен всем, а второй - по условию, условие уносят из блока в карту:

/etc/nginx/conf.d/shop.local.confhttp
map $arg_x $вложенный {
    default "";
    ~.      "да";
}

server {
    location /hdr {
        add_header X-Снаружи да always;
        add_header X-Внутри  $вложенный always;   # пустой не отдаётся вовсе
        return 200 "ok\n";
    }
}

Заголовок с пустым значением nginx не отправляет - значит одна строка заменяет и условие, и оба варианта ответа.

Разбор: один блок, два разных бэкенда

location /api/ {
    if ($arg_x) { proxy_pass http://back:8000; }
    proxy_pass http://back:8000/внешний/;
}

Конфиг грузится: nginx -t доволен. Замер того, что видит бэкенд:

Бэкенд печатает пришедший путь (return 200 "path = $request_uri"):

$  команда
curl -s http://localhost/внешний/cart
curl -s 'http://localhost/внешний/cart?x=1'
↳  выводдва curl выше: без параметра и с ?x=1
path = /внешний/cart
path = /api/cart?x=1

Один location отправляет запросы на два разных пути, и решает это параметр строки запроса. В конфиге об этом не написано ни слова: обе строки выглядят как один и тот же адрес. Разница в том, что у внешней есть путь /внешний/ (значит, подстановка URI), а у внутренней его нет (значит, URI уходит как есть).

Для сравнения - try_files внутри if nginx не принимает вовсе:

↳  выводnginx -t
nginx: [emerg] "try_files" directive is not allowed here in /etc/nginx/nginx.conf:7

То есть часть запрещений проверяется при старте, а часть - нет, и надеяться на nginx -t как на защиту нельзя.

Что ломается без этого

  • Два if подряд выполняются оба. set $r "${r}A" и set $r "${r}B" в двух условиях дали r=AB. Это не «первый выигравший», а последовательное выполнение на фазе rewrite - и потому длинные цепочки условий незаметно дорожают.
  • Проверка -f смотрит на $request_filename, то есть уже на собранный путь с учётом root и alias. В блоке с alias она проверит не тот файл, о котором идёт речь.
  • if в блоке server безопаснее, чем в location, но и там он остаётся фазой rewrite: return внутри него сработает раньше выбора location и раньше любых ограничений доступа.

Зачем это в работе

Правило «if is evil» появилось не из вкусовщины: у директивы нет своего контекста выполнения, она переиспользует механизм вложенного location. Поэтому на каждую задачу, ради которой тянутся к if, есть замена:

Задача Чем заменить
условие по имени домена отдельный блок server со своим server_name
условие по началу пути отдельный location
условие по методу запроса limit_except GET { deny all; }
«если файла нет» try_files $uri $uri/ /index.php;
условие по адресу клиента geo
всё остальное map

Оставшиеся законные применения if - редирект и перезапись по признаку, который иначе не выразить: if ($http_user_agent ~* бот) { return 403; }. Здесь внутри только return, значит неявный блок ничего не ломает.

Теперь сам

Найди, что не так, и скажи, при каком запросе это выстрелит:

location /files/ {
    add_header Cache-Control "public, max-age=3600" always;
    if ($arg_download) {
        add_header Content-Disposition attachment always;
    }
    alias /srv/files/;
}

Ответ: при ?download=1 из ответа пропадёт Cache-Control - останется только Content-Disposition. Сработавший if обслуживает запрос сам, а заголовки наследуются целиком или никак. Лечится картой: map $arg_download $раздача, и обе строки add_header стоят рядом на одном уровне.

Главное

Сработавший if - это вложенный блок, который забирает обслуживание запроса себе: внешние add_header пропадают, proxy_pass подменяется. Внутри безопасны return, rewrite, set, break. Условие по домену - отдельный server, по пути - отдельный location, по методу - limit_except, всё прочее - map.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.