Открытые ссылки медийки в MAX (про edge CDN)
Как делать правильно. Неправильно — когда медиа-контент мессенджера (социалки) доступен по прямой ссылке, и типа ее никто не знает.
Позволю себе оттоптаться по популярной теме, с решением.
Медиа-хайп вокруг популярного печально известного мессенджера в очередной раз пополнился дыркой, или «дыркой»: хранение и отдача медийки реализована по ссылкам, которые доступны снаружи из открытой сети.
Ссылка выглядит так: i.oneme.ru/i?&fn=ресайз&r=композитный_хеш
Результат — ну, например, вот такой: https://i.oneme.ru/i?&fn=w_1440&r=BTEFHNxXjmuR0N2Fir9SuMMRpSFWLfO_ieehECYcyBCPMsJPkexaioi5pUNQOIf47xc
Эта ссылка доступна прямо сейчас, безо всякой авторизации, всему интернету.
Кто-то исходную картинку внутри «защищенного мессенджера» разместил, или переслал. Картинка легла на медиа-сторадж, и отображается (самим мессенджером) по вот такой ссылке. Как-то же ее надо отображать, ну.
С одной стороны, подобрать вот этот самый хеш, без подготовки, достаточно сложно. Хотя и не сказать, что невозможно (об этом позже).
С другой стороны, вся «защита» строится на том, что получить тот самый целевой адрес вроде бы можно только в самом мессенджере. Если тебе картинку переслали, ты его знаешь. Если нет — удачи подобрать.
Так ли это? Нет, не так. Вот эту картинку я же как-то узнал, а?
Она точно не моя, мне ее никто не слал, у меня даже того мессенджера нет.
Проблема общая в том, что любая ссылка в интернете = штука открытая. Интернет так работает.
Ссылку, то есть урл запроса, видите вы, видит ваш клиент (мессенджера), это окей.
Ссылку видят все промежуточные узлы, которые роутят запрос, на любом уровне — да, у нас есть HTTPS шифрующий трафик, но есть и нюансики.
Готовую ссылку видит ваш браузер — и, например, шлёт «на индексацию», как регулярно делают пидорасы из Гугла и Яндекса.
Ссылку видит всё, что в этом браузере живет — все расширения, адблоки, плагины и прочий зоопарк. И так далее.
То есть, она уже не только «не секретная» — она уже совсем-совсем не секретная. И ее можно не подбирать, достаточно взять из логов готовую, работающую.
- С одной стороны, сервис вроде как нигде явно не обещал секретных ссылок.
- С другой стороны, переписка кого-то с кем-то в мессенджерах все-таки по привычке воспринимается, как что-то приватное.
Чтобы оценить масштабы бедствия:
https://web.archive.org/web/*/https://i.oneme.ru/i*
Не только Махом единым. Такая же, или аналогичная по сути своей схема исторически используется в офигеть каком множестве систем. Чисто исторически. И регулярно оказывается, что открытые всему интернету ссылки — они, вы не поверите, открытые всему интернету. Причем неограниченное время.
О чем, например, честно предупреждает гугл-докс по ссылке: «ссылка будет видна всему интернету». Но это же вдуматься надо.
Вот тут ребята в сеть слили почти миллион клиентских записей, принесли кейс в пример: https://www.a-tm.co.jp/en/news/44383/
Секретная ссылка с секретным алгоритмом хеша, энтропией, итд итп — перестает быть секретной, и становится security by obscurity, когда у злоумышленника уже есть эта ссылка готовая. Ее не надо генерить, вот она, бери. Никаких проблем.
Техническая проблема под этим всем следующая: edge CDN network.
Сервис не раздает картинки «сам». Объёмы медиа-контента обычно внушительные, он валяется где-то там кучей — и есть отдельная инфра, задача которой просто отдавать контент, не затрагивая логику приложения. То есть — CDN и его хосты, content delivery network.
CDN не содержит в себе значительного объема бизнес-логики, это неудобно операционно. Его задача — много хранить, и быстро раздавать. Все пред-обработки, включая проверки доступа, ресайзы, варианты, на CDN под каждый запрос делать весьма затратно. Они делаются заранее и генерятся статическую файло-помойку. Иногда запрашиваются от источника на первом обращении, но все равно потом складываются «под ноги» в кеш.
Для того, чтобы отдать статический файлик по урлу/хешу, надо просто поймать урл, убедиться что у нас такой есть, и отдать статику.
Для того, чтобы проверить доступ — надо где-то взять всё приложение целиком, там понять чей это контент, кто к нему стучится (аутентификация), какие у нас есть варианты доступа на этот счет, принять какое-то решение на этот счет (авторизация), и только потом отдать статику. Или не отдать.
Разница в нагрузках, геморроях и проверках — в разы, а то и на порядки. А CDN должен быть быстрым, большим и тупым. То есть, техническая сложность.
Но это только первая часть проблемы.
Если у себя держать большую-большую файлопомойку, то эта файлопомойка во-первых стоит эпических денег (контент никуда не девается, его становится только больше, косты растут). Во-вторых, этих файлопомоек становится несколько.
Например, чтобы разнести их по регионам, не гонять картиночки оптикой под океаном из Вирджинии юзеру в Москве.
Чтобы задублировать машинки, обеспечить количественно параллельность — то есть это отказоустойчивость, репликация итп.
Чтобы расшардить источники «по горячести», например популярных юзеров и каналы со 100500 подписчиков отдавать с одной инфры, а всякую личку и старьё холодное — с другой.
В-третьих, и это самое веселое, файлопомойки (в силу их тупости, цены и понятности задачи) могут быть вообще не ваши. А арендованные. В мире есть куча CDN-сервисов, которые предоставляют CDN как услугу. И вот там есть сотни региональных эндпойнтов, автоматическая репликация, машины размером с боинг и хардами объемом в петабайты, автоматическое управление источником (откуда брать вашу медийку).
Сотне тупых раздающих CDN-машин категорически насрать, что там за контент, откуда он взялся, и кому там чего можно. Что-то внутри себя они могут, но по очень небольшому списку (потому что все еще должны быть быстрыми, большими и тупыми). Ходить куда-то к вам в инфру и проверять права доступа — вся эта сетка не будет, при любом раскладе.
Ну т.е реально, базовый тариф edge Амазона это 120+ региональных площадок, которые ваш контент раздают за вас.
Медийка Akamai это сраный космолёт на пол-континента, и ей в душе па-ху-ю на суть того, что она раздает. У нее проблемы в экзабайтах измеряются.
Бегать в ваш сервис, или даже микросервис, никто не будет, ей запрещено.
Еще важно отметить в этой связи, что и бегать синхронизироваться с вашим медиа-источником удаленная CDN-сеть размером с акамай может, но не обязана — ввиду ее размеров. Первичный запрос — да, будет cache on read, CDN заберет исходник, к себе сложит. Если вы его там у себя поменяли, изменили, что-то еще танцевали — ей пахеру.
Даже удалять точечно контент там никто не будет, его дешевле хранить «условно вечно», чем бегать синхронизировать состояние каждого кусочка в отдельности. То есть, нюдсы, попавшие на CDN, останутся там навечно — мечта безопасника, практически. Есть полиси по времени жизни кусочков, но с ограничениями.
** Удобный лайфхак, кстати, у медиа-сервиса VK: если запросить свой собственный архив данных, вам отдадут ссылки на ваши же давно удаленные фоточки, и исходники, реально на CDN присутствующие. Можно слить бекапчик, ностальгии ради.
Подчеркну отдельно, чтобы было понятнее.
Варианта «а давайте на каждой картиночке за XYZ времени для всех юзеров всего приложения каждый раз полностью и полноценно проверять аутентификацию + авторизацию логикой приложения» — его не существует.
Задача не решается именно таким образом, даже на минимальных нагрузках и объемах. Надо упрощать.
Пути решения есть, причем два (простой и правильный, как обычно).
Первый путь — чисто на источниках-урлах-хешах (но не так, как у Макса, а посложнее)
Второй путь — на подписывании, и проверке content policy, когда CDN заранее имеет локально проверяемые правила для каждого юзера, юзер сам ему их приносит.
Путь дешманский
Чисто на источниках = означает, что кешированного контента по ссылкам, на CDN, должно стать меньше, и он оттуда должен удаляться совсем. Актуальный — перезапрашиваться заново, с вашего медиа-источника. Если ваш источник такого контента не отдал (уже не отдал), «вечная ссылка с нюдсами» работать уже перестанет, у нее источника нет.
Если CDN тупые, и тупыми останутся, можно попробовать поиграться с политикой кеширования — в смысле, массовой, для всего CDN целиком. Всей сетке говорим, что контент обязан устаревать через, например 3 дня, и в этом случае надо перезапрашивать исходник. Большинство коммерческих CDN такое худо-бедно умеют, правда в ограниченных рамках.
Если CDN совсем-совсем тупой, и не умеет в удаление никаким образом (так бывает) — можно динамически пересоздавать всю сеть, скриптами. Убивать ноды в CDN целиком, выводить из роутинга насовсем, пересоздавать новые. Да, они будут пустые, будет прогрев кеша и нагрузка на ваш источник — но зато сразу по актуальному состоянию.
Реализовывать правила per-user таким образом практически невозможно. Потому что CDN все еще отдает контент по ссылке в любом случае, ссылка утекла — здрасти. Всё, чего вы добьетесь, что можете ограничить доступ к источнику на своей стороне — и, например, проверять глобально права доступа хотя бы раз в день/неделю/месяц. Это все еще может быть удобно для глобальных настроек приватности. Ну и для GDPR, вы же помните, что юзерский контент хранить вечно нельзя.
Если есть возможность, лучше конечно так «простым путем» не делать, и «только ссылкам» не доверять. Но возможности наши разные бывают. Дешманское решение лучше, чем никакого.
Борьба с энтропией
Отдельно напомню, что саму ссылочку тоже надо не иметь возможности злонамеренно подобрать. То есть, я вон смотрю на структуру хеша в ссылке Макса, и вижу там 3 конкретных блока. Значит, чисто теоретически, они а) заменяемые б) подбирабельные. Если вы строите любой хеш по предсказуемым, детерминированным данным — например, id юзера — то смысла в таком хеше ровно ноль, он подбирается. То же самое по двум, трем, четырем параметрам: если они детерминированные, вопрос времени.
Если в хеше «соль», рандомная вставка, но статическая — тоже не оч хорошо, особенно если прям перебором видно, где она живет и на что влияет.
Время злонамеренного подбора чего-то с CDN вы ограничить «тупо ссылкой» скорее всего не можете.
Если я, или кто-то, начнет перебирать вам «хешированный урл» с CDN со скоростью = 1000 req/s, то есть 86.4 млн вариантов в день — коммерческая CDN-сетка этого даже не заметит, и алертить вам не будет. У них там масштабы другие.
То есть, светлая идея «добавим в хвост ссылки 6 рандомных символов из ее же md5()», в качестве «неподбираемой вставки», ломается за сколько? 16^6=16.7 млн вариантов, это за 5 часов на 1000 req/s.
Поэтому генерация ссылки, если уж вы делаете решение только на них (с очисткой кешей), должна иметь time-bound параметр. Или несколько. Например, сегодняшний день из даты, по UTC. Таким образом, закешированная ссылка, которую сгенерили позавчера, уже ничего не отдаст ни с CDN (где ее удалили очисткой кеша), ни с медиа-источника (где проверка хеша не пройдет). Авторизованный юзер может запросить и получить новую валидную, посторонний неавторизованный васян из интернетов — не может.
Ну и логичная оговорка, что вам по дефолту надо будет генерить 2 копии сразу, «за сегодня и за вчера», чтобы на переходе суток через полночь у вас весь контент не офигевал. Тут понятно.
Чем более variable источник хеша/ссылки/подписи, тем сложнее его ломать, и тем вам спокойнее спать. Детерминированных хешей не должно быть, «вечных» не-устаревающих не должно быть.
Путь правильный
Какую-то логику поручить CDN-ам, даже коммерческим и чужим, все-таки можно. Обычно ее крайне мало, и она по очень узкому списку. Потому что весь скриптинг и логика резко тормозят отдачу и усложняют сам CDN, портят всю малину вам или поставщику услуги.
Вычисление хешей. Вы можете поручить CDN-машине самостоятельно посчитать какой-то тупой хеш, и сравнить его с тем, что принесли в запросе (в ссылке). Там, где хеш не совпал — контент не отдавать. Почему хеш: потому что вычисление производится строго на ноде CDN, ей не надо бегать никуда в сеть и в ваши сервисы.
Например, нода должна поймать параметр $h=xxx, у себя вычислить hash (путь_файла + секрет + день_даты), и только при совпадении h==hash что-то отдать.
Это запросто реализуется однострочным скриптом в nginx/lua, либо коммерческие площадки такую логику у себя имеют; но вам, как сервису, придется под нее подстраиваться.
Соотв результат тот же, ссылка устаревает за день, но можно не чистить контент (а просто перегенерить ссылку на него, для авторизованного пользователя).
Куки и свойства браузера. Как правило, ноды CDN торчат в сеть самостоятельно, значит на них можно навесить доменный роутинг (cdn1.yoursite, cdn2.yoursite итп). Это означает, что при обращении юзера на cdn1.yoursite — на CDN — браузер получит всё то, что было ему отдано в cookie для домена *.yoursite.
Причем, именно в этот браузер, именно этому юзеру.
Это уже не ссылка, так просто не скопируешь.
Опа, у нас появляется механизм для разделения юзеров.
Конечно, просто записать в cookie «этому всё можно» не очень правильно, и совсем не секурно.
Но в дешманском варианте можно реализовать бакетинг: когда у вас ворох юзерспейсов, например чатиков с картинками, имеет доменный роутинг CHATID.cdn.yoursite, у CDN настроен wildcard *.cdn.yoursite (и ему пофиг на CHATID), а с базового домена вы выставляете юзерам cookie именно с полным CHATID, который содержит проверяемый ключ — для кусков контента, которые на CDN лежат.
Запрос к другому chatid уже не передаст настройку от предыдущего/соседнего, хотя все запросы придут все равно на один и тот же CDN.
Может, вообще запишем в куку права целиком?
Это мы только что придумали JWT. Который содержит в себе открытым текстом какую-то инфу, обычно про пользователя, и что ему можно (роли, например).
Рядом с инфой валяется алгоритмическая подпись payload-инфы, обычно асимметричного алгоритма. Открытым ключом можно только проверить подпись (подписать нельзя).
Соответственно, если у нас есть переданный (в cookie) кусок данных «чо можно», и есть возможность убедиться, что его туда поставил именно владелец ключа подписи (асинхронная криптография) — мы можем прямо на ноде принять какое-то решение по доступу, потому что все исходные данные у нас в руках. Бегать в родительскую систему не надо.
Вот это уже вариант, вполне себе. Ограничение будет только в том, что в cookie и прочий JWT сильно много данных не запихаешь, более 2Кб я бы не рассчитывал.
Для ролевых проверок, по ограниченному списку — сгодится, а в системах типа мессенджера, где ворох источников, юзеров, каналов, бакетов и настроек — уже начинается неудобно.
А что если мы совместим идею врЕменного хеша, как выше, с идеей подписывания правил доступа односторонним ключом?
Это мы уже придумали content policy, как у больших.
Мы можем в родительской системе написать правила прям явно: {файл, срок_доступа, юзер/роль}, и подписать асимметричной криптографией, положить подпись рядышком. Эта инфа сама по себе не секретная, как и ее подпись — просто пихаем их прямо в ссылку.
Вторым шагом мы в cookie для CDN, уже конкретному юзеру и его браузеру, пишем, что это вот такой-то {юзер/роль}, и тоже рядом генерим и кладем подпись.
Таким образом, задача проверки сводится к чему:
— взять правила из ссылки, проверить подпись, взять юзера из cookie-заголовка, проверить подпись, и сравнить одно с другим. И с календарным временем.
И вот это уже офигенный механизм, который может много-много чего.
У больших провайдеров он в том или ином виде есть. В смысле сейчас есть, штука появилась недавно. Но надо курить их доки, у всех свои детали и погремушки.
Европейскому Amazon когда-то (2019) лично я показывал пальцем, как это должно работать — понимаешь, внёс кусочек вклада своего опять, в мировые интернеты.
У них просто не было тогда, был какой-то тестово-пилотный вариант, а у нас пригорела жопа и стало срочно ннада.
Проверка асимметричной криптографии тоже штука не самая дешевая, но все-таки лучше (для коммерческой конторы) когда к тебе не бегают юзера со своим креативом, а когда ты сам родил предсказуемый механизм и сказал «во, пользуйтесь». Лишь бы ключ для проверки подписи под ногами на CDN валялся — и то, только открытый, потому что закрытый где-то там у автора контента в его системе живёт, он нам нафиг не нужен. Секурно.
Из последнего варианта с content policy уже можно накурить-накрутить себе что-то свое, произвольной развесистости.
WAF
Несложно заметить, что на том же принципе и механизме реализуется и web application firewall практически любого характера.
- Есть у юзера подписанный клочок данных, с нашей серверной подписью «этот = нормальный» и валидным временем — даем доступ к целевой системе.
- Нет валидного такого куска данных — заворачиваем на проверки, авторизации, пейволлы, и что вам там в жизни потребуется. Если всё окей, выставляем подписанный пащпорт «этот = нормальный».