SeniorДебаггингИногдаЕщё не отвечали
Этот обработчик загрузки аватарки доверяет запросу и тип файла, и путь сохранения — найдите и исправьте дефекты.
Эндпоинт загрузки аватарки принимает файл и сохраняет его для последующей отдачи.
Ограничения:
- сохранить контракт эндпоинта (multipart-загрузка, в ответе URL сохранённого файла);
- принимать только PNG и JPEG;
- сохранённый файл никогда не должен исполняться веб-сервером;
- не добавлять внешний сервис сканирования.
UPLOAD_DIR = "/var/www/html/uploads"
@app.post("/upload")
def upload():
f = request.files["file"]
if f.content_type not in ("image/png", "image/jpeg"):
abort(400)
dest = os.path.join(UPLOAD_DIR, f.filename)
f.save(dest)
return {"url": "/uploads/" + f.filename}
Найдите уязвимости в этом обработчике и исправьте их.
Три дефекта доверия. Заявленный тип задаёт клиент — определяем тип по байтам. Полученное имя склеивается с путём хранения — имя генерирует сервер. Каталог лежит в web-root — выносим наружу и отдаём без исполнения.
- ✗Починить проверку типа, оставив клиентское имя файла
- ✗Оставить каталог хранения внутри web-root
- ✗Сначала записать файл, а проверять его потом
- →Почему сгенерированное имя полностью снимает вопрос обхода каталога?
- →Что должен выставлять отдающий обработчик, чтобы браузер не трактовал файл?
Разбор
В обработчике три дефекта, и все они — доверие к данным запроса.
content_typeприходит из запроса, поэтому им объявить картинкой можно что угодно. Тип нужно определять по самим байтам.f.filenameсклеивается с путём хранения, поэтому имя способно увести запись за пределы каталога. Имя генерирует сервер.UPLOAD_DIRлежит внутри web-root, поэтому сохранённый файл отдаётся напрямую и может быть обработан как исполняемый. Каталог выносится наружу, а отдача идёт через собственный обработчик.
UPLOAD_DIR = "/srv/uploads" # вне web-root
EXT = {"png": ".png", "jpeg": ".jpg"}
@app.post("/upload")
def upload():
f = request.files["file"]
kind = imghdr.what(f.stream) # тип по байтам, не по заголовку
f.stream.seek(0)
if kind not in EXT:
abort(400)
name = uuid4().hex + EXT[kind] # имя от сервера, клиентское игнорируем
f.save(os.path.join(UPLOAD_DIR, name))
return {"url": url_for("serve_upload", name=name)}
Отдающий обработчик читает файл только по сгенерированному имени, ставит фиксированный Content-Type и Content-Disposition: attachment, а также X-Content-Type-Options: nosniff. Дополнительно стоит перекодировать принятое изображение и ограничить размер тела запроса.