open atlas
← Все проекты

backend · intermediate · 4d

Загрузка через presigned URL

Прямая загрузка в хранилище через presigned URL с ограничениями размера и content-type и webhook завершения, проверяющим, что объект действительно прибыл, — чтобы API-сервер никогда не касался байт файлов.

Проксирование загрузок через API-сервер — распространённая ошибка новичков: это тратит пропускную способность, блокирует потоки запросов и создаёт единую точку отказа. Presigned URL сдвигают тяжёлые байты напрямую в хранилище, оставляя авторизацию на сервере. Сложные части — это CORS, применение ограничений content-type до загрузки и подтверждение реальности объекта после прибытия.

Результат

Поток загрузки, где клиент получает presigned PUT URL, загружает напрямую в S3-совместимое хранилище, а сервер подтверждает получение, запрашивая метаданные объекта — никогда не проксируя байты файла.

Этапы

0/2 · 0%
  1. 01Выдай ограниченный presigned PUT

    Выдавай presigned PUT URL из API со сроком действия, разрешённым заголовком content-type и ограничением Content-Length.

    Критерии готовности
    • API возвращает presigned PUT URL с коротким сроком, фиксированным разрешённым content-type и максимальным Content-Length.
    • Просроченный или подделанный URL (не тот content-type / превышение размера) отклоняется хранилищем, а не API.
  2. 02Прямая загрузка + подтверждённое получение

    Настрой CORS на бакете, чтобы браузер мог загружать напрямую без прокси, затем проверяй прибытие объекта по его ETag и размеру в webhook.

    Критерии готовности
    • Браузер грузит напрямую в бакет (CORS-preflight проходит) без проксирования через API.
    • Webhook завершения подтверждает приход объекта, запрашивая его размер/ETag, и идемпотентен при повторных доставках.

Рубрика

Джуниор Миддл Сеньор
Ограничения presigned URL API выдаёт presigned PUT без ограничений; клиент может загрузить 5 ГБ видео там, где ожидалось изображение 2 МБ, или подставить произвольный content-type. URL подписан со сроком действия, фиксированным разрешённым Content-Type и максимальным Content-Length; подделанный или превышающий размер PUT отклоняется хранилищем, а не API — таким образом, принуждение встроено в подпись, а не в логику приложения на каждый запрос. Ты рассматриваешь злоупотребление перезаписью ключей: presigned PUT на предсказуемый ключ позволяет любому авторизованному пользователю перезаписать файл другого. Ты решаешь это серверно-назначенными UUID-ключами (никогда не выбираемыми вызывающим) и рассуждаешь об окне гонки между загрузкой и завершением: клиент может загрузить корректный файл, затем заменить его до срабатывания webhook, повторно использовав URL в пределах срока действия.
CORS и поток прямой загрузки Загрузка из браузера проксируется через API-сервер; каждая загрузка проходит через память приложения и блокирует поток запроса на всё время. CORS-политика S3-бакета разрешает браузерному источнику PUT напрямую; preflight OPTIONS-запрос проходит чисто, и ни один байт файла не касается API-сервера. Ты ограничиваешь CORS-политику до необходимого минимума: точные разрешённые источники, конкретные нужные заголовки (Content-Type, x-amz-*) и короткий max-age для кеша preflight. Ты объясняешь, почему wildcard CORS-политика на публичном бакете не является уязвимостью (нет кук, нет ambient authority), но почему она всё равно неверна на приватном бакете, где presigned URL несут авторизацию.
Webhook завершения и верификация получения Клиент самостоятельно сообщает о завершении; сервер доверяет сообщению и помечает загрузку как полученную, не проверяя, что объект существует или соответствует запрошенному. Webhook завершения запрашивает ETag и размер объекта из хранилища и подтверждает их соответствие ожидаемым значениям; webhook идемпотентен, так что повторная доставка при retry не приводит к двойной обработке. Ты рассуждаешь об окне подмены: между истечением presigned PUT и срабатыванием webhook существует гонка, когда другой файл может быть загружен на тот же ключ. Ты предотвращаешь это, сохраняя ожидаемый ETag (производный от предзагрузочного контракта) и отказываясь помечать получение как валидное, если сохранённый ETag отличается — и документируешь компромисс: верификация ETag ловит подмену, но ломает multipart-загрузки, где ETag собирается из хешей частей.
Эталонный разбор (спойлер)

Почему presigned PUT вместо проксирования: проксирование каждой загрузки через API-сервер привязывает один поток обработчика запросов на загрузку на всё время передачи. При 10 МБ/с и файле 100 МБ это 10 секунд времени потока на загрузку — сервер, обрабатывающий 100 параллельных загрузок, тратит 1000 поток-секунд на I/O. Presigned URL переносят этот I/O напрямую в объектное хранилище, которое масштабируется горизонтально с ничтожной стоимостью.

Принуждение content-type и размера должно быть в подписи, а не в webhook: webhook срабатывает после того, как байты уже в хранилище. Если ограничения проверяются только там, злоумышленник может сохранить скрипт под видом изображения, и приложение уже приняло его в свой бакет. Кодирование content-type и максимального размера в подписи presigned URL означает, что само хранилище применяет политику до записи единого байта.

Серверно-назначенные ключи предотвращают атаки перезаписи: если вызывающий выбирает ключ объекта (например, своё имя пользователя), он может перезаписать любой ранее загруженный файл или угадать ключ другого пользователя. UUID, сгенерированный сервером во время presign, сохранённый в записи ожидающей загрузки и проверяемый в webhook, делает ключ неугадываемым, а загрузку — непереигрываемой на другой слот.

Гонка завершения и верификация ETag: presigned URL действителен на всё своё TTL после выдачи. Клиент может загрузить легитимный файл, получить проходящий ETag от webhook, а затем загрузить другой файл на тот же ключ в оставшееся TTL URL. Сохранение ожидаемого ETag во время presign и отказ принимать получение, не совпадающее с ним, закрывает это окно — ценой несовместимости с multipart-загрузками, которые производят составной ETag.

Сделай по-сеньорски

  • Замени однофрагментный PUT на S3 multipart upload для файлов больше 5 МБ: создавай, загружай части параллельно и завершай — с хранением возобновляемого состояния на сервере.
  • Добавь серверную проверку на вирусы в webhook завершения через триггер Lambda/Worker перед пометкой загрузки как безопасной.

Навыки

presigned URL signingCORS preflight for direct uploadcontent-type + size enforcementwebhook idempotency

Рекомендуемый стек

nodehonoaws-sdk (S3-compatible)zod