Debian.pro/

Про Debian


Гостевая статья — 101 способ не ограничить доступ к ручке

Это достаточно старая статья, которая была когда-то давно опубликована в очень закрытом блоге (инвайт получить занимает минимум часов 10!). Планы по публикации её в публичный интернет так и не стали реальностью, поэтому статью мне пришлось стащить и… Шучу, я как минимум у двух человек уточнил (в том числе и у автора), они разрешили -)

Статью люто советую почитать всем — и периодически перечитывать.

Автор статьи — buglloc [https://github.com/buglloc] из лучшей ИБ-команды на Руси -)

Ниже оригинал (я только examples заменил и убрал несколько упоминаний непубличных вещей).

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

 


В прошлый раз я касался темы различной имплементации парсинга урлов в контексте OpenRedirect’ов. В этот же раз, я хотел бы продолжить схожую тему, но в контексте другой, менее распространенной (но не редко менее очевидной и более критичной) проблемы — контроль доступа при проксировании. Т.е. когда доступом к какой-то конкретной ручке хочется управлять уровнем выше приложения (e.g. nginx).

Суть проблемы, кроется в самом способе решения задачи:
• В приложении есть ручка /secret/place , к которой нужно ограничить доступ
• Не вопрос, добавим локейшен в nginx

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

Типичный список различий в парсинге пути, который может позволить обойти подобное ограничение:
• Путь может урлдекодироваться (e.g, /foo%2fbar -> /foo/bar ), а может и нет. Nginx декодирует.
• Слеши могут «склеиваться» (e.g. //foo -> /foo ), а могут и нет. Nginx клеит.
• Путь может нормализовываться (e.g. /foo/../bar -> /bar , /foo/./bar -> /foo/bar ), а может и нет. Nginx нормализует
• Обработка ошибок может отличаться (e.g. /foo% -> /foo или /foo% или 400ая, а может и вовсе /f%oo -> /foo ). Nginx, к примеру, отбрасывает % в конце, стейтмашина такая 🙁
• Применяется различная, странная нормализация (e.g. /foo\bar -> /foo/bar ). Это, зачастую, проблема реализации в конкретном приложении, фреймворки любят наставить непредсказуемых костылей для странных кейсов. Nginx нормализует \ только под win

Есть и второй, более редкий (но более багоопасный) случай — запретить запросы с определенными параметрами. В этом случае проблемы ровно теже, что и при Http Parameter Contamination/Pollution:
• Наличие или отсутствие урлдекодирования, e.g. имя параметра при запросе fo%6f=bar может быть как foo так и fo%6f . Nginx НЕ декодирует, имена параметров регистронезависимы (e.g. fOo=b%61r это переменная $arg_foo со значением b%61r )
• Различное поведение при дублировании foo=bar&foo=baz — первый, второй, «склеятся», образуют список, что либо еще? Nginx использует ПЕРВОЕ значение при наличии равно, например, для foo&foo=bar&&foo=baz будет $arg_foo = bar
• Различная обработка ошибок и нормализация, e.g. foo[bar=baz -> foo_bar=baz или foo%20=bar -> foo=bar . Nginx ничего не делает в данном случае

В свете всех этих проблем, прошу вас хорошенько подумать прежде чем соглашаться ограничивать доступ к проксируемому запросу. Скорее всего от этой затеи стоит отказаться и сделать ограничение доступа там, где этот запрос обрабатывается (e.g. в приложении, а не в nginx).

Дабы не быть голословным, рассмотрим несколько вариантов…

Входные данные:
• Nginx v1.10.1, NodeJS v6.2.2, Express v4.14.0
• Нужно ограничить доступ к ручке /secret/place

Код приложения:

var express = require('express');
var app = express();

// Вывод пути, для удобства дебага
function exposePath(req, res, next) {
    res.setHeader("X-App-Path", req.path);
    next();
}

app.get('/', exposePath, function (req, res) {
    res.send('index');
});

app.get('/secret/place', exposePath, function (req, res) {
    res.send('secret');
});

app.listen(3000, function () {
    console.log('Listening on port 3000!');
});

Базовый конфиг nginx:

server {
    listen 80;
    server_name cool-service.example.com;

    add_header "X-Nginx-ReqUri" $request_uri always;
    add_header "X-Nginx-DocUri" $document_uri always;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

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

location = /secret/place {
    deny all;
}

И с виду все работает:

user@server:~$ echo -e "GET /secret/place HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 403 Forbidden
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 13:53:15 GMT
Content-Type: text/html
Content-Length: 169
Connection: close

<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.10.1</center>
</body>
</html>

Однако, роутинг в Express сделан таким образом, что слеш в конце опционален, поэтому текущий запрет никому не помешает:

user@server:~$ echo -e "GET /secret/place/ HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:19:34 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 6
Connection: close
X-Powered-By: Express
X-App-Path: /secret/place/
ETag: W/"6-Xr4ilOzQ4PCOq3aQ0qbuaQ"
X-Nginx-ReqUri: /secret/place/
X-Nginx-DocUri: /secret/place/

secret

Хм, хоккей:) Переделываем на префиксный локейшен:

location /secret/place {
    deny all;
}

Ну вот! Теперь то точно работает:

user@server:~$ echo -e "GET /secret/place/ HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 403 Forbidden
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:20:51 GMT
Content-Type: text/html
Content-Length: 169
Connection: close

<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.10.1</center>
</body>
</html>

Правда, стоит еще раз взглянуть на реализацию роутинга в Express и увидеть, что он регистронезавизим, в то время как в nginx префиксный локейшен регистрозависим:

user@server:~$ echo -e "GET /secret/placE HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:31:25 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 6
Connection: close
X-Powered-By: Express
X-App-Path: /secret/placE
ETag: W/"6-Xr4ilOzQ4PCOq3aQ0qbuaQ"
X-Nginx-ReqUri: /secret/placE
X-Nginx-DocUri: /secret/placE

secret

Что ж, переделываем на регистронезависимый regexp:

location ~* ^/secret/place {
    deny all;
}

Пробуем:

user@server:~$ echo -e "GET /secret/placE HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 403 Forbidden
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:32:38 GMT
Content-Type: text/html
Content-Length: 169
Connection: close

<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.10.1</center>
</body>
</html>

Фуф! Справились! Пора идти за печенюшкой, но не все так просто;) Express использует для парсинга урла запроса модуль parseurl, а там целых два парсера под капотом — «простой» и стандартный. А зная, что стандартный модуль преобразует \ в / , осталось написать запрос, который бы спровоцировал parseurl воспользоваться именно им.
Пробуем раз:

user@server:~$ echo -e "GET /secret\\place HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 404 Not Found
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:30:39 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 25
Connection: close
X-Powered-By: Express
X-Content-Type-Options: nosniff

Cannot GET /secret\place

Не срослось:( Пробуем два:

user@server:~$ echo -e "GET /secret\\place# HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:30:51 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 6
Connection: close
X-Powered-By: Express
X-App-Path: /secret/place
ETag: W/"6-Xr4ilOzQ4PCOq3aQ0qbuaQ"
X-Nginx-ReqUri: /secret\place#
X-Nginx-DocUri: /secret\place

secret

То что нужно! 🙂



Устали от NodeJS? Я тоже, поэтому попробуем Flask 🙂

Входные данные:
• Nginx v1.10.1, Python v3.5.1, Flask v0.11.1, Gunicorn
• Нужно ограничить доступ к ручке /secret/place

Код приложения:

from flask import Flask
from flask import request

app = Flask(__name__)

@app.after_request
    def expose_path(response):
        response.headers['X-App-Path'] = request.path
        return response

@app.route('/')
    def index():
        return 'index'

@app.route('/secret/place')
    def secret_place():
        return 'secret'


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=3000)

Базовый конфиг nginx:

server {
    listen 80;
    server_name cool-service.example.com;

    add_header "X-Nginx-ReqUri" $request_uri always;
    add_header "X-Nginx-DocUri" $document_uri always;

    location / {
        # Отсутствие слеша в конце - важное условие!
        proxy_pass http://127.0.0.1:3000;
    }
}

Учитывая опыт с нодой, сразу переходим к последнему варианту:

location ~* ^/secret/place {
    deny all;
}

Что ж, варианты применяемые ранее не работают (и это отлично!), но не стоит отчаиваться:

user@server:~$ echo -e "GET ///foo/secret/place HTTP/1.0\n\n" | nc cool-service.example.com 80

HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Sun, 26 Jun 2016 14:52:56 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 6
Connection: close
X-App-Path: /secret/place
X-Nginx-ReqUri: ///foo/secret/place
X-Nginx-DocUri: /foo/secret/place

secret

Почему это работает? Это результат нескольких причин:
• Необходимо обратить внимание на proxy_pass в конфиге nginx, точнее на отсутствие URI (про отличие в доке: proxy_pass):
proxy_pass http://127.0.0.1:3000;
• Flask запущен Gunicorn’ом, т.к. при использовании uWSGI вы скорее всего будете передавать нормализованный PATH_INFO ($document_uri)
• Flask и Gunicorn парсят строку запроса как полноценную урлу, что дает сайд эффект за счет поддержки scheme-relative URLs
• Таким образом, в запросе ///foo/secret/place foo распарсился как netloc, а /secret/place как путь
И мы благополучно обошли проверку:)

В завершение
Это далеко не полный список проблем и различных вариантов обхода подобного рода ограничений. Это всего лишь небольшая подборка примеров, которая, я надеюсь, должна показать опасность подобной реализации. Большинство техник обхода очень зависит от используемых технологий и их комбинаций, так, например, переход на uWSGI/FastCGI может нивелировать многие проблемы (а может и нет, ибо зависит ^_^) за счет минимизации количества парсеров, но не уберечь от логики роутинга в приложении.

Хотелось бы еще раз сделать акцент на том, что установка ограничений к ручкам на уровне приложения выглядит более безопасной и устойчивой к переменам погоды на Марсе. Если с текущей версией приложения/окружения у вас все работает, это вовсе не означает что это не «разъедется» в будущем 🙁 Не стоит так же забывать, что мы не рекомендуем делать ограничения по IPшкам источничка запроса, лучше разрешать доступ к каким-то доп. ручкам после аутентификации и проверки провязки пользователя.



Upd. По результатам обсуждения в комментах (дисклеймер — комменты забрать из источника нельзя), чуть дополню
• это, ни в коем случае, не призыв отказываться от nginx и спускать _все_ в приложение
• это не относится к функционалу с которым nginx отлично справляется (rate limit, роботоловилка, etc)
• это не относится к случаям, когда средствами nginx делается хотфикс бага, который по каким-то причинам не может быть сиюминутно выложен
• это не призыв убирать дополнительные слои безопасности реализованные при помощи nginx. Наслоение средств защиты это прекрасно

В посте речь идет о тех случаях, когда nginx используется как единственное средство по контролю доступа к отдельно взятым ручкам (эта практика порой используется в сервисах и я хотел предостеречь вас от этого). В данном случае, полагаться только на nginx может иметь неприятные последствия и именно поэтому, принимая решение где это самое ограничение лучше реализовать, стоит еще раз все обдумать и, скорее всего, сделать свой выбор в пользу приложения.
Если же средствами nginx делается какое-то дополнительное ограничение — это просто замечательно, это подход в котором ничего не нужно менять. К примеру:
• ручка доступна неким условным «модераторам» и за это несет ответственность приложение
• nginx же _дополнительно_ ограничивает доступ к этой ручке внутренней сетью
Стоит ли тут что либо менять? Думаю нет 🙂

07.07.2022 byinkvizitor68sl|mustread

Комментариев пока нет.

Написать комментарий