1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7

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

Подписанные ссылки и защита от хотлинка

Коротко

Два случая, где ни пароль, ни фильтр по адресу не годятся: платный файл, ссылка на который разошлась по чатам, и твои картинки на чужом сайте.

  • secure_link проверяет подпись в самом адресе: приложение решает, кому можно, а отдаёт файл nginx.
  • $secure_link принимает три значения: пустая строка - подделка, 0 - просрочено, 1 - можно.
  • valid_referers смотрит на заголовок Referer. Замер: чужой источник - 403, свой и пустой - 200.
  • Referer присылает клиент, поэтому это защита от трафика, а не от человека.

Если знаешь про подписанные ссылки и понимаешь слабость Referer - листай до «Теперь сам».

Сначала ответь сам

Платный отчёт лежит файлом. Ссылку выдают после оплаты, но через час она есть в трёх чатах. Пароль ставить некуда - файл качают программы, сессии у них нет. Что делать?

Положить решение в саму ссылку. Приложение подписывает адрес секретом, который знает и nginx; подпись содержит срок годности. Подделать нельзя - секрета нет; а через час ссылка перестаёт работать сама, и разошедшаяся по чатам копия превращается в 410.

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

Билет в кино с временем сеанса и защитной печатью. Контролёру не нужна база покупателей: он смотрит на печать и на время. Билет можно передать другу - это не именной документ, - но после сеанса он не годится никому.

Механизм: подпись вместо запроса

приложение знает секрет, решает «кому можно» nginx знает тот же секрет, проверяет и отдаёт файл /files/report.pdf?md5=...&expires=... за отдачу приложение не платит: файл едет мимо него и ни одного запроса к приложению на каждое скачивание
Отличие от auth_request: решение вшито в ссылку, отдельного подзапроса нет.

Правило

/etc/nginx/conf.d/shop.local.confhttp server location /files/
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 пишут в круглых скобках, с пробелами вокруг =.

Разбор: чего подпись не умеет

Ссылку можно переслать. Пока не истёк срок, она работает у кого угодно - это не привязка к пользователю. Хочешь привязку - добавляй в подпись адрес клиента:

/etc/nginx/conf.d/shop.local.confhttp server location /files/
secure_link_md5 "$secure_link_expires$uri$remote_addr my-secret-key";

Цена решения: человек за мобильным оператором может сменить адрес посреди скачивания, и ссылка отвалится. Для больших файлов это реальный сценарий.

MD5 здесь не про стойкость хеша. Смысл в том, что без секрета подпись не собрать. Секрет надо беречь и менять при утечке - а менять его значит разом сломать все выданные ссылки, поэтому срок им ставят короткий.

Разбор: valid_referers и его слабость

/etc/nginx/conf.d/shop.local.confhttp server location ~* \.(jpg|jpeg|png|webp)$
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 присылает клиент, поэтому это защита трафика, а не доступа.

Комментарии

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

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

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