Русский
Заметка, которую сервер принял за шрифт
О чём это: сервер trip2g отказался принять научную статью про иммунологию, потому что решил, что это шрифт. Разбираемся, почему так вышло, и почему проверка, которая выглядела разумной, для заметок не годилась вовсе. Читать, если вы проверяете загружаемые файлы по их содержимому.
Мы гоняли бенчмарк поиска на наборе SciFact: 5183 аннотации научных статей. Каждую превратили в заметку и залили в trip2g обычной синхронизацией. Залились все, кроме одной пачки из ста штук. Сервер вернул ошибку:
Unsupported content type: application/vnd.ms-fontobject
application/vnd.ms-fontobject — это Embedded OpenType, формат шрифтов из времён Internet Explorer. В пачке были только тексты про клетки и белки. В логе сервера нашлась виновная заметка:
---
title: "BASOPHILS AND THE T HELPER 2 ENVIRONMENT CAN PROMOTE..."
---
Как текст стал шрифтом
Перед сохранением заметки сервер проверял её содержимое функцией http.DetectContentType из стандартной библиотеки Go. Функция смотрит на первые 512 байт и сверяет их со списком известных сигнатур. У PNG это \x89PNG в начале, у GIF — GIF89a, у MP3 — ID3. Если ни одна сигнатура не совпала и бинарных байтов нет, функция отвечает: «текст». Сервер пропускал только текст и HTML.
У шрифта EOT сигнатура необычная. Первые 34 байта могут быть любыми, а 35-й и 36-й должны быть заглавными LP. Посчитаем: ---, перевод строки, title: " — это 12 байт. Значит, 35-й и 36-й байты файла — это 23-я и 24-я буквы заголовка. В «BASOPHILS AND THE T HELPER 2» там стоят L и P из слова HELPER. Для функции это шрифт.
Сама функция не ошибается. Она делает ровно то, для чего написана: реализует алгоритм, которым браузер угадывает тип ответа, когда сервер не прислал заголовок Content-Type. Браузеру нужно решить, показать ответ как картинку, проиграть как звук или вывести как текст. Лучшая догадка там лучше, чем никакой.
У нас вопрос другой. Мы не угадываем, что прислали: мы знаем, что это заметка, и хотим убедиться, что внутри действительно текст. А произвольный текст рано или поздно совпадёт с чьей-нибудь сигнатурой:
| Начало заметки | Что решила функция |
|---|---|
--- + title: "BASOPHILS AND THE T HELPER 2" |
шрифт EOT |
ID3 tags explained: how MP3 metadata works |
звук MP3 |
GIF89a is an image format from 1989 |
картинка GIF |
<?xml version="1.0"?> notes about XML |
документ XML |
# Заметка |
текст |
Заметка про теги MP3, которая начинается со слова ID3, — вполне жизненный случай. С EOT случайность уже не такая редкая, как кажется: хватит аббревиатуры NLP или слова HELP, заглавными, на нужной позиции в заголовке.
Хуже всего было сообщение
Пользователь, с чьей заметкой такое случилось, видел: «Unsupported content type: application/vnd.ms-fontobject». В сообщении нет ни слова про заголовок, про 35-й байт или про то, что поправить. Заметка просто не синхронизируется, соседние синхронизируются. Мы наткнулись на это случайно, на пяти тысячах чужих текстов. Живой пользователь с одной такой заметкой, скорее всего, решил бы, что сломался синк.
Что поменяли
Все форматы, которые принимает синхронизация, — текстовые: Markdown, HTML, JSON-разметка холстов и баз. Поэтому гадать по сигнатурам незачем. Достаточно проверить кодировку: содержимое должно быть корректным UTF-8 и без нулевых байтов.
if !utf8.ValidString(update.Content) || strings.ContainsRune(update.Content, 0) {
return &model.ErrorPayload{Message: "File content must be UTF-8 text"}
}
Картинка, бинарник или файл в другой кодировке под видом .md по-прежнему не пройдут: в PNG есть и нулевые байты, и невалидные для UTF-8 последовательности. А текст со словом HELPER пройдёт, потому что текстом он и был.
Проверка по содержимому хороша, когда вы не знаете, что пришло. Когда знаете, проверять надо то, что обещано.