Цели дизайна
Valine — изящная, простая в использовании и эффективная при развёртывании система комментариев. Когда я впервые с ней познакомился, меня привлекли её изящный стиль и serverless-архитектура. Ей не нужен серверный сервис — фронтенд напрямую работает с хранилищем LeanCloud, и это действительно здорово! Но чем глубже я её изучал, тем больше видел проблем.
Проблемы Valine
Закрытый исходный код
Начиная с версии 1.4.0 автор публикует в репозитории GitHub только скомпилированные файлы, а исходный код перестал обновляться. Возможно, автор разочаровался в open source. Для таких пользователей, как я, которые хотят добавить или изменить что-то в проекте, это довольно неприятная ситуация.
XSS
С самых ранних версий пользователи сообщали о проблемах XSS в Valine, и сообщество пыталось решать их разными способами: добавляло капчи, фильтрацию XSS на фронтенде и другое. Однако позже автор пришёл к выводу, что любая проверка на фронтенде защищает только от честных людей, поэтому ограничения вроде капчи были убраны.
Сейчас, когда пользователь отправляет комментарий, Markdown на фронтенде преобразуется в HTML, затем к нему применяется функция фильтрации XSS, и только после этого данные отправляются в LeanCloud. Полученные из LeanCloud данные вставляются в DOM напрямую. Очевидно, что такой процесс небезупречен. Если HTML отправляется напрямую и отображается сразу после получения, XSS невозможно устранить в корне.
Принципиальное решение
Для хранимых XSS-атак можно навсегда избавиться от проблемы, экранируя HTML-коды. Как в старом BBCode, в базу данных попадает только содержимое Markdown. Фронтенд читает содержимое, преобразует Markdown в HTML и перед отображением кодирует весь HTML.
function encodeForHTML(str) {
return ('' + str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}Поскольку Valine — это serverless-система, атакующий может напрямую обратиться к этапу хранения. Любые меры предосторожности до сохранения данных бесполезны, обработать данные можно только при отображении. Поскольку после экранирования HTML не может быть разобран, можно гарантировать, что преобразованный HTML не будет вставлен.
Поскольку Valine больше не имеет открытого исходного кода, отправить pull request невозможно.
Так как описанный выше подход полностью ограничивал бы пользователей рамками Markdown, начиная с 0.15.0 Waline добавляет на клиенте DOMPurify для защиты от XSS. Помимо обычной очистки XSS:
<form>и<input>отключены;- внедрение стилей запрещено;
- автовоспроизведение медиа отключено;
- все внешние ссылки обрабатываются и открываются в новом окне с атрибутом
rel, равнымnoopener noreferrer.
Утечка конфиденциальных данных
Помимо прямого доступа к хранилищу, атакующий может напрямую прочитать любые данные. Если поле базы данных имеет права на чтение для всех, его содержимое полностью прозрачно для атакующего.
В данных комментария два поля — IP и почта — содержат конфиденциальные данные пользователя. Mr.Deng написал специальную статью с критикой этой проблемы: Пожалуйста, немедленно прекратите использовать систему комментариев Valine.js, пока в ней не будет исправлена утечка персональных данных. Ещё раньше, когда сообщество JueJin использовало LeanCloud, была выявлена проблема безопасности с раскрытием номера мобильного телефона пользователя.
Чтобы обойти эту проблему, автор Valine добавил конфигурацию recordIP, определяющую, разрешено ли записывать IP-адрес пользователя. Поскольку сервера нет, решить проблему можно только отказом от хранения значения.
У этого варианта всё же есть недостаток: записывать ли IP, решает владелец сайта, а пользователи, оставившие комментарии, не имеют права управлять собственной конфиденциальностью.
Утечка адреса электронной почты — ещё одна серьёзная проблема конфиденциальности. Вычислить md5 адреса электронной почты пользователя на фронтенде и получить по нему аватар Gravatar вполне реально. Но если нужно отправлять уведомления по электронной почте, когда на комментарий ответили, приходится хранить исходное значение адреса. Теоретически эту проблему можно решить шифрованием RSA: закрытый ключ хранится в переменной окружения LeanCloud, клиент отправляет и md5 адреса, и адрес, зашифрованный открытым ключом. Когда LeanCloud нужно отправить уведомление, облачная функция читает закрытый ключ из окружения и расшифровывает адрес пользователя. Однако с учётом размера и производительности библиотеки RSA для фронтенда это нереализуемо. Добавление серверного слоя, который фильтрует конфиденциальную информацию на стороне сервера, — гораздо более правильное решение.
Подмена статистики чтения
В Valine 1.2.0 появилась статистика прочтений статьи: при посещении страницы пользователем количество визитов записывается по URL в таблицу counter в бэкенде. Поскольку данные нужно обновлять при каждом обращении к странице, права должны допускать запись, иначе последующие обновления поля невозможны. Отсюда возникает проблема: фактически данные можно обновить до любого значения. Если это интересно, откройте официальный сайт https://valine.js.org/visitor.html, зайдите в консоль и попробуйте выполнить следующий код. Не забудьте после эксперимента вернуть число обратно~
const counter = new AV.Query('Counter');
const resp = await counter.equalTo('url', '/visitor.html').find();
resp[0].set('time', -100000).save();
location.reload();К счастью, значение поля time имеет тип Number, поэтому вставить другие значения нельзя. Если бы поле time было строкового типа, это могло бы стать уязвимостью XSS. Возможное решение этой проблемы — отказаться от накопительного способа хранения. Вместо этого сохранять доступную только для чтения запись о каждом визите и использовать метод count() для подсчёта при чтении. Тогда все данные доступны только для чтения, что решает проблему подмены. У этого решения тоже есть недостаток: при относительно большом объёме данных возникает определённая нагрузка на запросы.
Если же нужно сохранить исходные данные, остаётся только добавить серверный слой, изолирующий права на изменение.
Рождение Waline
По перечисленным причинам и появился Waline. Изначально целью Waline было лишь добавить Valine серверную часть, но поскольку Valine не имеет открытого исходного кода, реализовать это удалось только средствами фронтенда. Разумеется, многие части кода и логика фронтенда заимствованы у Valine, чтобы оставаться совместимыми с его конфигурацией. Даже название проекта я образовал от Valine, чтобы всем было понятно: это производный от Valine проект.
Помимо решения перечисленных проблем безопасности, появление серверной части позволило реализовать множество функций, которые ранее были невозможны без сервера, включая уведомления по электронной почте, фильтрацию спама в комментариях и другое.
