Gixy — open source от Яндекса, который сделает конфигурирование Nginx безопасным

Gixy — open source от Яндекса, который сделает конфигурирование Nginx безопасным

Nginx, однозначно, один из крутейших веб-серверов. Однако, будучи в меру простым, довольно расширяемым и производительным, он требует уважительного отношения к себе. Впрочем, это относится к почти любому ПО, от которого зависит безопасность и работоспособность сервиса. Признаюсь, нам нравится Nginx. В Яндексе он представлен огромным количеством инсталляций с разнообразной конфигурацией: от простых reverse proxy до полноценных приложений. Благодаря такому разнообразию у нас накопился некий опыт его [не]безопасного конфигурирования, которым мы хотим поделиться.

Но обо всем по порядку. Нас давно терзал вопрос безопасного конфигурирования Nginx, ведь он — полноправный кубик веб-приложения, а значит, и его конфигурация требует не меньшего контроля с нашей стороны, чем код самого приложения. В прошлом году нам стало очевидно, что этот процесс требует серьезной автоматизации. Так начался in-house проект Gixy, требования к которому мы обозначили следующим образом:

— быть простым; — но расширяемым; — с возможностью удобного встраивания в процессы тестирования; — неплохо бы уметь резолвить инклюды; — и работать с переменными; — и про регулярные выражения не забыть. Признаться, мы до последнего колебались с выбором языка (между Golang и Python). В итоге был выбран Python с надеждой на то, что он более распространен, а значит, будет чуть проще с развитием.

О проблемах

На этом покончим со вступлением и перейдем к примерам распространенных проблем :) Чтобы избежать путаницы в будущем, во всех примерах использовалась текущая mainline версия Nginx — 1.13.0.

Server-Side-Request-Forgery

Server Side Request Forgery — уязвимость, позволяющая выполнять различного рода запросы от имени веб-приложения (в нашем случае от имени Nginx). Возникает, когда злоумышленник может контролировать адрес проксируемого сервера — например, в случае некорректной настройки XSendfile.

По своему опыту могу сказать, что зачастую уязвимость связана с несколькими ошибками:

— отсутствие директивы internal. Ее смысл заключается в указании того, что определенный location может использоваться только для внутренних запросов; — небезопасное внутреннее перенаправление.

Если с первым случаем все понятно, то с внутренним перенаправлением дела обстоят не так просто. Полагаю, многие из вас видели/писали подобную конфигурацию:

К сожалению, в такой конфигурации вам необходимо проверить как минимум все директивы rewrite и try_files, так как согласно документации:

Получается, любой неосторожный реврайт позволит сделать запрос в internal location. В этом довольно легко убедиться:

В данной ситуации мы обычно рекомендуем несколько практик:

— использовать только internal location для проксирования; — по возможности запретить передачу пользовательских данных; — обезопасить адрес проксируемого сервера:

• если количество проксируемых хостов ограничено (например, у вас S3), то лучше их захардкодить и выбирать при помощи map или иным удобным для вас образом; • если по какой-то причине нет возможности перечислить все возможные хосты для проксирования, его стоит подписать.

Плохие регулярные выражения для валидации реферера или ориджина

Нередко валидация заголовка запроса «Referer» или «Origin» делается при помощи регулярного выражения. Зачастую это необходимо для условного выставления заголовка X-Frame-Options (защита от ClickJacking) или реализации Cross-Origin Resource Sharing (CORS). И если с валидацией «Referer» все немного проще и, с некоторыми условиями, можно отказаться от регулярного выражения в пользу модуля ngx_http_referer_module, то с «Origin» не все так однозначно.

Мы выделяем два основных класса проблем:

— ошибки в составлении регулярного выражения; — разрешение недоверенных third-party доменов.

Проблемная конфигурация выглядит следующим образом:

На самом деле я очень упростил регулярное выражение, но даже в этом примере увидеть проблему с первого раза не столь просто. Людям проще писать регулярные выражения, нежели читать их.

К счастью, машине несвойственна эта проблема, поэтому Gixy умеет самостоятельно определять, что это регулярное выражение сматчит www.yandex.ru.evil.com как валидный origin и сообщит вам об этом:

Или, если считать ya.ru недостаточно доверенным, сообщит о ориджинах ya.ru и www.yandex.ru.evil.com:

HTTP Splitting

HTTP Splitting используется для атак на приложение, стоящее за Nginx (HTTP Request Splitting), или на клиентов приложения (HTTP Response Splitting). Уязвимость возникает в случае, когда атакующий может внедрить символ перевода строки \n в запрос или ответ, формируемый Nginx.

Безотказного совета (кроме как быть внимательными) у меня нет, но всегда следует обращать внимание на несколько вещей:

— какие переменные используются в директивах, отвечающих за формирование запросов (могут ли они содержать CRLF), например: rewrite, return, add_header, proxy_set_header и proxy_pass; — используются ли переменные $uri и $document_uri, и если да, то в каких директивах, так как они гарантированно содержат урлдекодированное значение; — уделить особое внимание переменным, полученным из групп с исключающим диапазоном: (?P[^.]+).

Пример с исключающим диапазоном:

Как вы видите, мы смогли добавить заголовок ответа x-crlf-header: injected. Это случилось благодаря стечению нескольких обстоятельств:

— add_header не кодирует/валидирует переданные ему значения, считая, что автор знает о последствиях; — значение пути нормализуется перед обработкой локейшена; — переменная $action была выделена из группы регулярного выражения с исключающим диапазоном: [^.]*; — таким образом, значение переменной $action стало равно see below\r\nx-crlf-header:injected и попало в HTTP-ответ.

К счастью, Gixy с немалым успехом справляется с этой задачей:

— он знает об «опасных» переменных — точнее, он знает о допустимом множестве символов в большинстве встроенных переменных. Таким образом, отличие $request_uri от $uri для него очевидно; — умеет выделять переменные из групп регулярного выражения; — умеет определять, может ли какой-либо символ (в нашем случае \n) сматчиться регулярным выражением (или отдельно взятой группой).

Другой интересный пример — реврайт с помощью try_files:

— эксплуатация (на 127.0.0.1:9000 слушает отладочный echo-сервер):

— Старайтесь использовать более безопасные переменные, например $request_uri вместо $uri. — Запретите перевод строки в исключающем диапазоне, например /some/(?[^/\s]+) вместо /some/(?[^/]+. — Возможно, хорошей идеей будет добавить валидацию $uri (только если вы знаете, что делаете).

Переопределение «вышестоящих» заголовков ответа директивой add_header

Это известная особенность Nginx, о которую спотыкались и будут продолжать спотыкаться многие из нас. Суть крайне проста — если у вас устанавливаются заголовки на одном уровне (например, в серверной секции), а уровнем ниже (например, в локейшене) устанавливаются какие-либо еще, то первый не будет применен.

Наиболее простой пример выглядит следующим образом:

В данном случае заголовок ответа X-Content-Type-Options не будет установлен при обработке локейшена /.

Gixy с успехом расскажет вам об этом:

Мне известно несколько способов решить эту проблему:

— продублировать важные заголовки; — устанавливать заголовки на одном уровне, например в серверной секции; — рассмотреть вариант с использованием модуля ngx_headers_more.

Каждый из них имеет свои преимущества и недостатки. Какой предпочесть, зависит от вас.

О Gixy

Надеюсь, я вас убедил в том, что конфигурация Nginx требует более пристального внимания. Я также верю в то, что статический анализ конфигураций Nginx может работать (это также подтверждает опыт Nginx Amplify). К сожалению, не всегда есть возможность автоматически определить все пограничные случаи или специфичные особенности приложения, стоящего за Nginx. Так, к примеру, я не стал включать в стандартный набор проверку переопределения заголовков запроса X-Forwarded-*, так как реакция на них зависит от приложения, а в некоторых случаях к ним и вовсе нельзя притрагиваться (например, при множественном проксировании). Но у себя вы можете сделать нужные вам проверки, основываясь на более глубоком понимании работы приложения. Да, сейчас Gixy не умеет определять весь спектр известных нам проблем, но учится и, возможно, с вашей помощью начнет делать это лучше и полнее.

Если же говорить о сценариях использования, то для себя мы выделили несколько типовых случаев:

— запуск в тестовой среде, где установлен nginx; — веб-приложение для проверки отдельно взятого блока. Это бывает полезно, когда вам встретился подозрительный участок конфига; — HTTP API для интеграции с CI или тонкими клиентами.

Нам кажется, что наиболее интересен вариант с использованием HTTP API для тонких клиентов. Ведь в таком случае мы можем централизованно управлять нужными нам проверками, обновлять их и так далее. К счастью, современные версии nginx обладают ключом -T для тестирования конфигурации и дампа оной, а Gixy умеет парсить этот формат.

$ nginx -T | http -v https://gixy/api/check Content-Type:'application/nginx' POST /api/check HTTP/1.1 Accept: application/json, */* Accept-Encoding: gzip, deflate Connection: keep-alive Content-Length: 959 Content-Type: application/nginx Host: gixy User-Agent: HTTPie/0.9.8

# configuration file /etc/nginx/nginx.conf: user http; worker_processes 1;

#daemon on; events

http

Напоследок хотелось бы подчеркнуть тот факт, что это первая публичная alpha-версия Gixy, поэтому API может изменяться без сохранения обратной совместимости. В связи с этим, если у вас есть необходимость в реализации собственного плагина, лучше написать Issue или прислать Pull Request — тогда мы вместе что-то придумаем.

Надеюсь, наш опыт был вам интересен и полезен, и, быть может, даже заставил пересмотреть свои конфигурации еще раз;)

📎📎📎📎📎📎📎📎📎📎