You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
12 KiB
162 lines
12 KiB
8 years ago
|
---
|
||
|
title: nginx auth_request (3/3): Web-Application firewall, WAF
|
||
|
tags: nginx, software, devel, репост
|
||
|
---
|
||
|
|
||
|
- [Вводная](/articles/2017/05/16/nginx-authreq-1/)
|
||
|
- [Многофакторная авторизация, NFA](/articles/2017/05/17/nginx-authreq-2/)
|
||
|
- [WAF, Web-Application firewall](/articles/2017/05/18/nginx-authreq-3/)
|
||
|
|
||
|
Сразу оговорюсь - я знаю про существование `nginx naxsi` и `mod_security`, речь про то, как вообще такое делается.
|
||
|
|
||
|
Под WAF я понимаю некое дополнение к веб-серверу, выполняющее одну или несколько следющих задач:
|
||
|
|
||
|
* блокировка вредоносных запросов
|
||
|
* подтверждение юзером "сомнительных" запросов
|
||
|
* ограничение частоты запросов по сложным критериям
|
||
|
|
||
|
Дополнительно можно собирать статистику.
|
||
|
|
||
|
---
|
||
|
|
||
|
Принцип работы всё тот же - nginx сначала пересылает копию запроса на какой-то внешний сервис
|
||
|
и на основании ответа от этого сервиса - решает, пропускать ли исходный запрос или нет.
|
||
|
|
||
|
Механизм работы опсан в предыдущей статье, поэтому интересны прежде всего алгоритмы:
|
||
|
1) выделение запросов одного пользователя,
|
||
|
2) ограничения частоты запросов.
|
||
|
3) определения "вредоносности" или "подозрительности" запроса,
|
||
|
Нетрудно заметить, третья задача зависит от первой, а вторая и третья тесно связаны.
|
||
|
|
||
|
По порядку.
|
||
|
|
||
|
Выделение запросов одного пользователя
|
||
|
--------------------------------------
|
||
|
|
||
|
Первая и очевидная мысль, приходящая в голову - просто смотреть запросы с одного ip.
|
||
|
Но тут есть множество тонкостей. Случаев, когда с одного адреса могут сидеть несколько
|
||
|
пользователей - достаточно много: nat, vpn, tor, ipv6-to-4 брокер.
|
||
|
И обратный случай - когда один юзер может сидеть с нескольких адресов: tor.
|
||
|
|
||
|
Что делать? На заголовки полагаться нельзя, там можно подделать абсолютно всё.
|
||
|
Комбинация ip+заголовок, например User-Agent? Это ещё хуже, подделкой заголовка можно добиться,
|
||
|
чтобы система определяла тебя как разных юзеров, даже не меняя свой ip.
|
||
|
|
||
|
Один из возможных вариантов - заставить каждого неопознанного клиента произвоить некую ресурсоёмкую операцию,
|
||
|
после выполнения которой этому клиенту присваивается уникальный идентификатор,
|
||
|
который тот будет предъявлять в дальнейшем как доказательство выполненной работы.
|
||
|
Разумеется, его нужно защитить от подделки и ограничить срок годности.
|
||
|
|
||
|
Блок схема ([исходник](schema-1.dot)):
|
||
|
|
||
|
[![](schema-1_tn.png)](schema-1.png)
|
||
|
|
||
|
Путь "известного" юзера показан жирной линией, "неизвестного" - прерывистой.
|
||
|
Операция (3) может быть к/либо вычислением на стороне пользователя, с сообщением результата.
|
||
|
Или же - проверкой на робота, например следование по редиректам. сделанных средствами самого html.
|
||
|
|
||
|
Схема может изменяться в некоторых пределах, например (2) "N" может переходить не в (4), а в (3).
|
||
|
Можно например, "неизвестных" клиентов сразу кидать на капчу, т.е. (1) "N" -> (4).
|
||
|
|
||
|
Думаю не нужно напоминать, что все операции, кроме (3) следует оптимизировать на минимальные
|
||
|
затраты ресурсов и максимальное быстродействие. Если используется капча, её лучше нагенерить заранее
|
||
|
и продумать механизм "карантина" на какое-то время для показанных, но не решённых.
|
||
|
|
||
|
Обратите внимание, путь по жирной линии выполняется в пределах одного обработчика.
|
||
|
А вот переходы к другим состояниям - требуют перехода на другие странички.
|
||
|
Помните, что я говорил про `error_page` и поддержку редиректов?
|
||
|
|
||
|
Ограничение частоты запросов
|
||
|
----------------------------
|
||
|
|
||
|
Здесь может быть куча вариантов реализации.
|
||
|
|
||
|
Например самое простое и железобетонное решение:
|
||
|
выделять временн*ы*е слоты определённой длительности
|
||
|
и считать запросы юзера в пределах текущего слота.
|
||
|
|
||
|
my $len = 5; # новый слот каждые 5 минут
|
||
|
my $time = time();
|
||
|
# вычисляем имя слота
|
||
|
my $slot = sprintf "req:%d:%d", $len, ($time - ($time % ($len * 60)));
|
||
|
# увеличиваем число запросов для юзера с $uuid
|
||
|
# и узнаём, сколько запросов он уже сделал в пределах данного слота
|
||
|
my $reqs = $redis->hincr($slot, $uuid => 1); # O(1)
|
||
|
$redis->expire($slot, 3600) if $reqs == 1;
|
||
|
# если $reqs >= $limit - блокируем запрос
|
||
|
|
||
|
Метод хорош тем, что крайне прост (1 запрос), хорошо масштабируется
|
||
|
и может работать вообще без обслуживания.
|
||
|
Основной недостаток - низкая "разрешающая способность",
|
||
|
на границе временного слота можно превысить лимит *до* 2х раз.
|
||
|
|
||
|
Если нам нужна гарантия, что в каждый момент времени лимит не будет превышен, подход несколько другой.
|
||
|
На каждого юзера заводится по персональному списку, туда пишется время запросов.
|
||
|
Недостатки: стоимость выше, больший расход памяти.
|
||
|
|
||
|
Принцип действия такой: при частых запросах в начале очереди растёт число "недавних" запросов.
|
||
|
Как только N-ый элемент оказывается "недавним", значит лимит превышен.
|
||
|
|
||
|
Вариант 2/а:
|
||
|
|
||
|
my ($time, $limit) = (5, 60); # время окна в минутах и количество запросов
|
||
|
my $now = time();
|
||
|
my $key = sprintf "user:%s", $uuid;
|
||
|
my $some = $redis->lindex($key, $limit - 1); # O(N)
|
||
|
my $next = $some + ($time * 60);
|
||
|
if ($now > $next) {
|
||
|
$redis->lpush($key, $now); # O(1)
|
||
|
# периодически подрезаем список, чтоб не разрастался сверх меры
|
||
|
$redis->ltrim($key, 0, $limit - 1); # O(N), где N - количество удалённых элементов
|
||
|
$redis->expire($key, 3600) if $some == 0;
|
||
|
} else {
|
||
|
# лимит превышен
|
||
|
}
|
||
|
|
||
|
Вариант 2/б, где "гарантированно дорогой" lindex() с O(N),
|
||
|
заменяется на llen() + lindex(-1), т.е. 2 x O(1).
|
||
|
Хотя в теории, lindex(N) для списка в котором меньше N элементов - тоже должен отрабатывать за O(1).
|
||
|
|
||
|
my ($time, $limit) = (5, 60); # время окна в минутах и количество запросов
|
||
|
my $now = time();
|
||
|
my $key = sprintf "user:%s", $uuid;
|
||
|
my $len = $redis->llen($key); # O(1)
|
||
|
if ($len < $limit) {
|
||
|
# первичное заполнение
|
||
|
$redis->lpush($key, $now); # O(1)
|
||
|
$redis->expire($key, 3600) if $len == 0;
|
||
|
return;
|
||
|
} else {
|
||
|
$redis->ltrim($key, 0, $limit - 1); # O(N), где N - количество удалённых элементов
|
||
|
my $last = $redis->lindex($key, -1); # O(1)
|
||
|
my $next = $last + ($time * 60);
|
||
|
if ($now >= $next) {
|
||
|
$redis->lpush($key, $now);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
# лимит превышен
|
||
|
|
||
|
Впринципе, всё это экономия на спичках. Значительно большего эффекта можно добиться,
|
||
|
если считать не абстрактные "запросы", а прикинуть стоимость конкретного запроса
|
||
|
в плане нагрузки и выделять пользователю "бюджет" на пользование.
|
||
|
|
||
|
Например, показ странички с картинками, где половина содержимого - статика,
|
||
|
а остальное закешировано - это одно. Полнотекстовый поиск по сайту - это уже другое.
|
||
|
А попытка авторизации на сайте - совсем даже третье.
|
||
|
|
||
|
"Вредоносность" и "подозрительность" запроса
|
||
|
--------------------------------------------
|
||
|
|
||
|
Здесь сложно дать какие-то рекомендации, смотрите по ситуации.
|
||
|
Можно анализировать заголовки (User-Agent/Referer/Accept/...), частоту запросов, их логическую взаимосвязь.
|
||
|
Например, запрос к автодополнению с X-Requested-With - это с высокой вероятностью человек.
|
||
|
Постоянная долбёжка поля поиска с интервалом в секунду - практически наверняка бот-дудосер.
|
||
|
Монотонные запросы несвязанные друг с другом - поисковый бот.
|
||
|
Пиковые всплески запросов с интервалом в несколько минут - юзер, который открывает несколько вкладок сразу, а потом сидит их читает.
|
||
|
|
||
|
Вобщем, на практке быстро научитесь на что нужно смотреть.
|
||
|
|
||
|
Ну и не стоит забывать про типовые запросы для поиска админок вордпресса, скуэля, гостевух и прочего говнокода на похапе.
|
||
|
Клиенту, спалившемся на таком можно просто возвращать 404 на все запросы, чтоб больше не приходил.
|