Від завантаження до URL: безпечне зберігання та обслуговування файлів користувачів у Express
Дізнайтеся, де програми Express повинні зберігати завантажені файли, як express.static відображає папку у URL-адресах, та які заходи безпеки запобігають тому, щоб завантаження користувачів стали вразливістю.
Користувач обирає фото профілю, натискає «Завантажити», і через секунду воно з’являється на його сторінці. За цією простою взаємодією стоять три справжні дизайнерські рішення: де фізично знаходиться файл, як сервер знову його знаходить та як браузер відображає його. У цьому посібнику розглядаються всі три аспекти для застосунку Express — від вибору місця зберігання до доступу до файлів через URL — а також наводяться заходи обережності, які запобігають тому, щоб завантаження стали джерелом загроз.
Зберігання завантажених файлів на диску
Перш ніж файл можна буде знову надати, його потрібно десь записати. У найпростішій конфігурації це папка в власній файловій системі сервера, яка зазвичай називається uploads та розташовується у корені проекту поруч із вихідним кодом, а не всередині нього:
project-root/
uploads/
avatars/
user-42.png
user-91.png
documents/
invoice-108.pdf
src/
app.js
Групування файлів за призначенням, таким як avatars/ та documents/, швидко приносить користь. Це дозволяє підтримувати перелік каталогів у контрольованому стані зі зростанням кількості файлів, полегшує застосування правил очищення та резервного копіювання за категоріями, а також, як ми скоро побачимо, створює URL-адреси, які легко передбачити. Одна плоска папка, що містить усі типи файлів, стає складною для керування задовго до того, як виникнуть проблеми з продуктивністю.
У більшості додатків база даних зберігає лише посилання на файл, наприклад відносний шлях avatars/user-42.png, а не сам файл чи повну URL-адресу. Зберігання відносного шляху дозволяє згодом змінити домен чи перейти на інший сервіс зберігання без необхідності переписувати кожен рядок.
Локальний диск чи зовнішнє об’єктне сховище
Зберігання файлів на сервері додатку
Запис файлів безпосередньо на диск сервера, як показано вище, не вимагає додаткової інфраструктури. Це ідеальний варіант для локальної розробки, прототипів, внутрішніх інструментів та додатків, обсяг завантажень яких є невеликим та прогнозованим.
Переміщення файлів у сервіс зберігання
У продакшн-системах завантаження частіше передаються у спеціалізований сховище об’єктів, наприклад Amazon S3, Google Cloud Storage або подібний сервіс, щоб диск сервера додатку не був залучений до цього процесу.
Чому важливий вибір
- Диски можуть бути невеликими та тимчасовими. Багато платформ хостингу перебудовують або замінюють інстанції після кожного розгортання, тому файл, який існує лише на локальному диску, може зникнути під час наступного розгортання.
Усе це не робить локальне сховище неправильним. Для навчальних проектів та справді невеликих додатків це розумний початковий крок, а описані нижче принципи (структуровані шляхи, передбачувані URL-адреси, ретельна перевірка) залишаються незмінними під час міграції на зовнішнє сховище. Зберігання коду сховища у невеликому модулі з функціями на кшталт save та getUrl значно полегшує цю міграцію.
Використання express.static для обслуговування папки
Після збереження файлу браузер все ще потребує способу його отримання. Express постачається з проміжним сервісом для статичних файлів саме для цієї мети:
const express = require("express");
const app = express();
app.use("/uploads", express.static("uploads"));
Цей рядок підключає каталог uploads під префікс URL /uploads. Для будь-якого запиту, шлях до якого починається з цього префікса, Express шукає файл у відповідному місці всередині папки, і якщо він існує, передає його назад з відповідним типом контенту та заголовками кешування. Вам не потрібно писати окремий обробник для кожного файлу.
Це відрізняється від звичайного маршруту API. Замість виконання логіки, яка завантажує дані та формує відповідь, ви публікуєте цілий каталог, який відповідає за принципом шляху. Із цього випливають два практичні моменти:
- Аргумент з папкою визначається відносно каталогу, з якого запущений процес Node, а не відносно файлу, що містить код. Використання абсолютного шляху, наприклад такого, який створюється за допомогою
path.join(__dirname, „..“, „uploads“), усуває несподіванки, коли додаток запускається з іншого робочого каталогу. - Усе, що знаходиться в цій папці, стає загальнодоступним. Підключайте лише той каталог, який має бути видимим.
Як збережений файл стає URL
Після налаштування статичного обслуговування шлях на диску та шлях у URL відображають один одного:
File on disk: uploads/avatars/user-42.png
Accessible at: https://yourapp.com/uploads/avatars/user-42.png
Завдяки цьому однозначному зв’язку фронтенд може безпосередньо посилатися на файл, наприклад у тегу зображення, без необхідності використання окремого API-кінцевої точки для завантаження даних:
<img src="https://yourapp.com/uploads/avatars/user-42.png" alt="User avatar" />
Саме тут структура папок з першого розділу виявляє свою корисність. Будь-який формат, який ви оберете на диску, є точно таким самим, яким клієнти бачать його у URL-адресах, тож охайна та послідовна структура забезпечує охайні та послідовні посилання. Це також означає, що URL можна сформувати на основі відносного шляху, збереженого в базі даних, що дозволяє зберігати домен та префікс у одному значенні конфігурації.
Заходи безпеки для завантажень користувачів
Завантаження є улюбленою мішенню для зловживань, тому факт «функціональності завантаження» є лише початком. Кожен з наведених нижче заходів усуває певну проблему.
Перевірте, що насправді є файлом
Розширення імені файлу — це просто текст, обраний користувачем, і воно нічого не доводить. Перевіряйте справжній тип контенту, бажано шляхом читання перших байтів файлу, а не покладаючись на оголошений тип MIME, та приймайте лише те, що потрібно функціоналу. Наприклад, кінцева точка для аватарів має приймати лише зображення, і ніщо більше. Формати на кшталт SVG та HTML вимагають особливої обережності, оскільки браузери можуть виконувати скрипти, вбудовані в них.
Обмежте розмір
Встановіть максимальний розмір для кожного завантаження. Без обмежень одна надто велика заявка, чи то випадкова, чи навмисна, може заповнити диск або перевантажити пропускну здатність мережі. Запровадьте це обмеження під час парсингу заявки, щоб сервер припинив читання раніше, замість того щоб спочатку прийняти весь контент.
Створюйте власні імена файлів
Ніколи не створюйте файл під ім’ям, яке надав клієнт. Це ім’я може містити дивні символи або послідовності для проходження по шляху, такі як ../../config.js, призначені для виходу за межі каталогу завантажень. Створіть нове ім’я на сервері — наприклад, випадковий ідентифікатор разом з перевіреною розширенням. Це також запобігає тому, що двоє користувачів, які завантажують photo.jpg, перезапишуть один одного.
Тримайте каталог завантажень ізольованим
Зберігайте завантажені файли у каталозі, який чітко відокремлений від вихідного коду та конфігурації, і переконайтеся, що статичний мідлвейр відкриває лише цей каталог. Випадкове підключення до кореня проекту призведе до оприлюднення файлів середовища та вихідного коду для будь-кого, хто зможе вгадати шлях.
Подумайте, звідки надходять файли
Надання контенту, наданого користувачем, з того ж домену, що й основний додаток, може призвести до проблем на кшталт крос-сайту скриптінгу, оскільки зловмисний файл буде виконуватися під іменем вашого сайту. Тому багато продакшн-систем передають завантажені файли з зовнішнього сховища або окремого піддомену. Надсилання заголовка X-Content-Type-Options: nosniff та, для завантажень, які не повинні відображатися внутрішньо, заголовка Content-Disposition: attachment, ще більше зменшує ризик.
Поширені запитання
Чи повинен кожен проект використовувати S3 або подібний сервіс?
Не обов’язково. Для невеликих або навчальних проектів локальний диск є простішим та цілком достатнім. Зовнішнє сховище стає важливим, коли ви розгортаєте проект на платформі з тимчасовою файловою системою або запускаєте кілька інстанцій, які повинні бачити однакові файли.
Чи потрібен додатковий пакет для express.static?
Ні, це вбудовано в Express. Аналіз надісланих файлів з форми типу multipart зазвичай вимагає використання бібліотеки на кшталт Multer, але обслуговування вже збережених файлів — ні.
Чи допустимо зберігати початкову назву файлу?
Краще цього не робити. Ім’я, створене сервером, уникає дивних символів, зіткнень ідентичних назв та хитрощів, пов’язаних із спеціально сформованою назвою файлу. Якщо користувачам потрібно бачити початкову назву, зберігайте її як метадані в базі даних.
Чи можна обмежити доступ до завантажених файлів?
Так, але не лише за допомогою express.static, який передає файл будь-кому, хто знає його URL. Для приватних файлів потрібен спеціальний маршрут, який перевіряє автентифікацію та авторизацію перед тим, як транслювати файл, або, у випадку зовнішнього зберігання, тимчасові посилання, створені лише для дозволених користувачів.
Підсумок
Надійна функція завантаження відповідає по черзі на чотири запитання. Де знаходиться файл: на диску сервера через простоту, чи в сховищі об’єктів через здатність до виживання після оновлень та масштабування? Як додаток знаходитиме його пізніше: за допомогою організованої структури папок та відносного шляху, збереженого в базі даних? Як браузер отримає його: через express.static, прив’язаний до чіткого URL-префікса, чи через захищений маршрут для приватних файлів? І що забезпечує його безпеку: перевірка типів, обмеження розміру, імена, створені сервером, та вузько визначена, бажано окрема, локація обслуговування. Якщо це вирішено, решта функціоналу — це переважно технічні деталі.