Loading...

Безопасность · 9 мин чтения

Подписанные ссылки: ссылки, которые перестают работать

Подписанная ссылка несёт доказательство того, что её выдали именно вы, и момент, когда она умирает. Это тот самый контроль, который нужен для файлов, за которые платят. В этом руководстве — два семейства подписанных ссылок, точный рецепт на cdn.com.tr и ошибка с кэшем, из-за которой каждая защищённая загрузка идёт на ваш origin.

Обновлено

Подписанные ссылки: ссылки, которые перестают работать

Видео на английском с русскими субтитрами.

Что на самом деле обещает подписанная ссылка

Три вещи, и здесь стоит быть точным, потому что люди обычно ждут четвёртую.

Она доказывает, что ссылка от вас. Подпись — это хеш от пути, срока действия и секрета, который знает только ваш сервер. Без секрета никто не подделает рабочую ссылку.

У неё есть срок действия. Срок — часть того, что подписывается, поэтому изменить его, не сломав подпись, нельзя.

Её проверяют до обращения к вашему origin. На edge CDN неподписанный запрос отклоняется прямо на edge; ваш сервер его вообще не увидит.

Чего она не обещает: что человек, открывший ссылку, не сохранит файл и не отправит его другу по почте. Подписанная ссылка контролирует доступ к URL, а не к самим байтам. Если нужен контроль после скачивания, вам нужен DRM — совсем другой и гораздо более тяжёлый продукт.

Два семейства: подписи на edge и presigned URL

Подписи пути на edge — то, что даёт CDN. Ваше приложение вычисляет подпись для пути и срока действия; edge проверяет её при получении запроса. Файл может лежать где угодно, куда дотягивается CDN, а проверка происходит рядом с посетителем.

Presigned URL от объектного хранилища — то, что даёт S3 и S3-совместимые хранилища. Сам сервис хранения выдаёт временную ссылку, подписанную вашим ключом доступа, обычно на несколько минут. Подпись едет в строке запроса как X-Amz-Signature и её соседи.

Это не конкуренты. Разумная и распространённая схема — объектное хранилище за CDN: подпись на edge защищает публичный URL, а бакет остаётся приватным, чтобы никто не мог обойти edge. Чего делать не стоит — выставлять presigned-ссылку хранилища напрямую наружу и считать, что её защищает CDN: если ссылка ведёт прямо на бакет, edge вообще не участвует в пути запроса.

Как это работает на cdn.com.tr

Это настройка на правиле доставки, поэтому можно защитить /downloads, оставив остальной сайт публичным. Включите истекающие ссылки для этого правила, задайте секрет и генерируйте ссылки в своём приложении.

Подпись — это base64url-форма от «сырого» MD5 трёх вещей, склеенных вместе: срока действия, пути и секрета с одним пробелом перед ним.

Три исхода, и они намеренно различны, чтобы их можно было различить по логам: верная подпись с неистёкшим сроком получает файл, который кэшируется как обычно; запрос без подписи или с неверной подписью получает 403; верная подпись с истёкшим сроком получает 410.

410 значит больше, чем кажется. Когда клиент говорит «ваша ссылка не работает», один только код ответа показывает, отправили ли ему нерабочую ссылку или он просто слишком долго ждал, — и для этого не нужно ничего у него уточнять.

Генерация подписанной ссылки на PHP

$uri     = "/downloads/report.pdf";
$expires = time() + 600;                   // ten minutes
$secret  = "your-long-random-secret";

$token = rtrim(strtr(base64_encode(
    md5($expires . $uri . " " . $secret, true)
), "+/", "-_"), "=");

$url = "https://cdn.example.com{$uri}?md5={$token}&expires={$expires}";

Ловушка кэша, которая обесценивает CDN

Это та часть, которой не хватает в большинстве документаций, и именно она бьёт больнее всего.

Каждая подписанная ссылка уникальна: свой md5 и свой expires для каждого посетителя и каждой выдачи. Если ключ кэша включает строку запроса — а это обычное поведение по умолчанию, и у нас тоже, пока вы это не измените, — то каждый подписанный запрос становится отдельной записью в кэше. Доля попаданий для защищённых файлов падает до нуля, каждая загрузка идёт на ваш origin, и вы платите за CDN, который на деле работает как обычный прокси.

Исправление — это одна настройка. В правиле доставки задайте режим обработки строки запроса так, чтобы параметры подписи не попадали в ключ кэша:

- Список игнорируемых — игнорировать md5 и expires, оставить всё остальное значимое. Правильный выбор, если тот же файл запрашивают ещё и с настоящими параметрами. - Игнорировать всё — самый простой вариант, когда у защищённых путей вообще нет значимых параметров, что обычно верно для загрузок.

Проверяйте это, а не считайте само собой разумеющимся: запросите один и тот же файл дважды с двумя разными действительными подписями и посмотрите на заголовок статуса кэша. Второй запрос должен быть попаданием. Если оба — промахи, значит подпись всё ещё в ключе.

Как выбрать срок действия

Достаточно коротко, чтобы утёкшая ссылка была бесполезна; достаточно долго, чтобы загрузка успела завершиться на плохом соединении.

Документы и изображения: от пяти до пятнадцати минут. Их запрашивают сразу после клика.

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

Манифесты стриминга требуют аккуратности. В HLS плеер сначала запрашивает манифест, а затем — множество сегментов на протяжении всего воспроизведения. Если подписывать каждый сегмент с коротким сроком действия, воспроизведение обрывается где-то в середине длинного видео. Подписывайте со сроком, который покрывает весь сеанс целиком, либо подписывайте только манифест, а сегменты защищайте другим способом.

Чего нельзя сделать — отозвать одну конкретную выданную ссылку. Она действительна до истечения срока, и точка. Если нужен немедленный отзыв, меняйте секрет — это аннулирует сразу все выданные ссылки.

Ошибки, из-за которых утекает секрет

Подписывать в браузере. Если секрет лежит в JavaScript, он публичный. Подписывать нужно только на сервере, всегда. Звучит очевидно, но это самый частый способ утечки секретов.

Зашивать ссылки на этапе сборки. Ссылка, сгенерированная во время статической сборки, несёт срок действия, установленный в момент сборки, и он истекает, пока страница ещё живая. Генерируйте ссылку в момент запроса или через небольшой эндпоинт, который делает редирект.

Рассинхронизация часов. Срок действия сверяется с часами edge. Если часы вашего сервера приложения отстают на пару минут, ссылки рождаются уже истёкшими. Следите, чтобы NTP работал; если ссылка не срабатывает сразу после выдачи, проверьте часы раньше, чем код.

Хранить секрет в репозитории. Относитесь к нему как к учётным данным: переменная окружения, а не система контроля версий. Когда кто-то покидает команду, меняйте секрет.

Забыть, что смена секрета глобальна. Смена секрета разом аннулирует все выданные ссылки, включая те, что уже пять минут как ушли в письмах. Это именно то, что нужно во время инцидента, и именно то, что не нужно случайно в пятницу под вечер.

Как настроить

Пошаговые руководства в справочном центре:

Частые вопросы

В чём разница между подписанной ссылкой и presigned URL?

В основном — где проходит проверка. «Presigned URL» — это термин S3 для временной ссылки, которую выдаёт и проверяет само хранилище. Подписанная ссылка на edge CDN проверяется на edge, рядом с посетителем, и работает для любого origin, а не только для бакета. Концепция та же: подпись плюс срок действия в строке запроса.

Может ли кто-то передать подписанную ссылку дальше?

Да, пока она не истекла. Подпись доказывает, что ссылку выдали вы, но ничего не говорит о том, у кого она сейчас на руках. Короткий срок действия ограничивает ущерб. Если нужно привязать ссылку к конкретному человеку, включите в подписываемый путь что-то идентифицирующее и проверяйте это на своей стороне — и смиритесь с тем, что настроенный на это человек всё равно сможет переслать уже скачанный файл.

Подписи вредят кэшированию?

Только если подпись попадает в ключ кэша, и тогда вредят сильно: каждая уникальная ссылка превращается в отдельную запись кэша, и каждая загрузка идёт на ваш origin. Настройте режим строки запроса так, чтобы параметры подписи игнорировались, — и файл будет кэшироваться как обычно, общим для всех, у кого есть действующая ссылка.

Можно ли отозвать одну ссылку?

По отдельности — нет. Подпись действительна до истечения срока, потому что проверка не хранит состояние: нет списка, из которого её можно было бы удалить. Доступные рычаги — короткий срок действия и смена секрета, которая аннулирует всё сразу.

Разве MD5 не «сломан» для этой задачи?

MD5 «сломан» в смысле стойкости к коллизиям, а это важно, когда атакующий может выбирать оба сообщения. Здесь же атакующему нужно подделать хеш строки, содержащей секрет, которого у него нет, — это задача поиска прообраза, а не коллизии. Реальный риск — утечка секрета или то, что его можно подобрать: используйте длинный случайный секрет, храните его только на сервере и периодически меняйте.

Подходит ли это для HLS-видео?

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