# теория · шаг 5 из 7
Подписанные ссылки и защита от хотлинка
Коротко
Два случая, где ни пароль, ни фильтр по адресу не годятся: платный файл, ссылка на который разошлась по чатам, и твои картинки на чужом сайте.
secure_linkпроверяет подпись в самом адресе: приложение решает, кому можно, а отдаёт файл nginx.$secure_linkпринимает три значения: пустая строка - подделка,0- просрочено,1- можно.valid_referersсмотрит на заголовокReferer. Замер: чужой источник - 403, свой и пустой - 200.Refererприсылает клиент, поэтому это защита от трафика, а не от человека.
Если знаешь про подписанные ссылки и понимаешь слабость Referer - листай до «Теперь сам».
Сначала ответь сам
Платный отчёт лежит файлом. Ссылку выдают после оплаты, но через час она есть в трёх чатах. Пароль ставить некуда - файл качают программы, сессии у них нет. Что делать?
Положить решение в саму ссылку. Приложение подписывает адрес секретом, который знает и nginx; подпись содержит срок годности. Подделать нельзя - секрета нет; а через час ссылка перестаёт работать сама, и разошедшаяся по чатам копия превращается в 410.
На что это похоже
Билет в кино с временем сеанса и защитной печатью. Контролёру не нужна база покупателей: он смотрит на печать и на время. Билет можно передать другу - это не именной документ, - но после сеанса он не годится никому.
Механизм: подпись вместо запроса
Правило
secure_link $arg_md5,$arg_expires;
secure_link_md5 "$secure_link_expires$uri my-secret-key";
if ($secure_link = "") { return 403; } # подпись не сошлась
if ($secure_link = "0") { return 410; } # сошлась, но время вышло
# $arg_md5 и $arg_expires - это параметры ?md5=...&expires=... из адреса ссылки
root /var/www/protected;
$secure_link после проверки принимает одно из трёх значений: пустая строка -
подделка, 0 - просрочено, 1 - всё в порядке. Ссылку выдаёт приложение: берёт
срок (unix-время), склеивает с URI и секретом, считает MD5, кодирует в
base64url. Порядок частей в secure_link_md5 (сначала срок и URI, в конце
секрет) обязан точь-в-точь совпадать с тем, как приложение считает подпись,
иначе она не сойдётся; пробел перед секретом - обычный разделитель, он тоже
входит в подписываемую строку. Секрет живёт в двух местах - в конфиге nginx и в
настройках приложения.
Это тот редкий случай, когда if уместен: внутри только return, и это одна из
четырёх безопасных директив из тринадцатого разделаРазбирается в разделе 13, глава «return против rewrite»: Фазы: кто и когда решает.
if пишут в круглых скобках, с пробелами вокруг =.
Разбор: чего подпись не умеет
Ссылку можно переслать. Пока не истёк срок, она работает у кого угодно - это не привязка к пользователю. Хочешь привязку - добавляй в подпись адрес клиента:
secure_link_md5 "$secure_link_expires$uri$remote_addr my-secret-key";
Цена решения: человек за мобильным оператором может сменить адрес посреди скачивания, и ссылка отвалится. Для больших файлов это реальный сценарий.
MD5 здесь не про стойкость хеша. Смысл в том, что без секрета подпись не собрать. Секрет надо беречь и менять при утечке - а менять его значит разом сломать все выданные ссылки, поэтому срок им ставят короткий.
Разбор: valid_referers и его слабость
valid_referers none blocked server_names shop.local *.shop.local;
if ($invalid_referer) { return 403; }
Замер на стенде:
| запрос | ответ |
|---|---|
без Referer |
200 |
Referer: http://evil.com/p |
403 |
Referer: http://shop.local/p |
200 |
Слово none в списке и делает первую строку разрешающей: браузер не всегда
присылает Referer (переход по прямой ссылке, политика приватности), и без
none ты отрежешь часть своих же пользователей. blocked - про заголовок,
вырезанный корпоративным прокси.
Слабость очевидна: Referer присылает клиент. Кто угодно подставит нужное
значение одной строкой в curl. Это защита от трафика, а не от человека:
чужой сайт со вставленной картинкой перестанет её показывать, а
целенаправленно скачать файл по-прежнему можно.
Что ломается без этого
Секрет в двух местах разъезжается. Поменяли ключ в приложении, забыли в nginx - все ссылки становятся 403. Обратный порядок так же плох. Ключ хранят там, откуда его берут оба (переменная окружения, секрет в системе развёртывания), а не копируют руками.
Срок в прошлом из-за часового пояса. $secure_link_expires - unix-время,
без зон. Ошибка на три часа даёт мгновенно просроченные ссылки; признак -
ровно 410 на все выданные ссылки сразу после выкладки.
Проверка на статике, которая раздаётся с CDN. Если картинки идут через CDN,
Referer до nginx не доедет вовсе, а secure_link сломается на кешировании:
у каждой подписанной ссылки свой адрес, и кеш перестаёт попадать.
Зачем это в работе
Три инструмента этой главы решают три разные задачи, и путать их не стоит:
| задача | инструмент |
|---|---|
| «пускать только своих по адресу» | allow / deny |
| «пускать по паролю или из офиса» | auth_basic плюс satisfy any |
| «решает внешний сервис» | auth_request |
| «решает приложение, а отдаёт nginx» | secure_link |
| «не показывать картинки на чужих сайтах» | valid_referers |
Последние два - не про безопасность в строгом смысле, а про экономику: трафик, нагрузка, оплаченный контент.
Теперь сам
Подписанные ссылки работают, но пользователи с мобильного жалуются: скачивание
файла на 500 МБ обрывается на середине с ошибкой доступа. В подпись включён
$remote_addr. Что происходит?
Мобильный оператор меняет адрес клиента посреди скачивания (или клиент переключается с Wi-Fi на сотовую сеть). Запрос на докачку приходит уже с другого адреса, подпись не сходится - 403. Для больших файлов адрес в подпись не включают, а срок делают коротким; либо привязывают к подсети, а не к точному адресу.
Главное
secure_link кладёт решение в саму ссылку: приложение подписывает адрес
секретом со сроком годности, nginx проверяет подпись и отдаёт файл, не дёргая
приложение. $secure_link даёт пустую строку при подделке, 0 при просрочке и
1 при успехе. Ссылку можно переслать, пока она жива; привязка к $remote_addr
это чинит, но ломает докачку при смене сети. valid_referers отсекает показ
картинок на чужих сайтах - обязательно со словом none в списке, иначе отрежешь
своих, - но Referer присылает клиент, поэтому это защита трафика, а не
доступа.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий