timeago.js форматирует даты в строки и записывает их в DOM через
textContent. Это безопасный механизм. Однако уязвимости
возникают в коде вокруг библиотеки, а не в ней самой.
const el = document.querySelector('time');
el.textContent = '<script>alert(1)</script>'; // Безопасно!
textContent всегда записывает обычный текст — HTML-теги
экранируются браузером автоматически. Выводимое значение
"<script>alert(1)</script>" будет отображено
как текст, а не выполнено.
timeago.js использует именно textContent при
render. Прямого XSS через саму библиотеку нет.
// Опасно! — XSS уязвимость
const label = format(post.createdAt, 'ru');
el.innerHTML = `Опубликовано: ${label}`; // Если label содержит HTML — XSS
// Безопасно
el.textContent = `Опубликовано: ${format(post.createdAt, 'ru')}`;
// Или
const span = document.createElement('span');
span.textContent = 'Опубликовано: ';
const time = document.createElement('time');
time.textContent = format(post.createdAt, 'ru');
el.appendChild(span);
el.appendChild(time);
// Опасно — если timeAgo содержит пользовательские данные
const timeAgo = format(post.createdAt, 'ru');
return <div dangerouslySetInnerHTML={{ __html: timeAgo }} />;
// Безопасно
return <time>{format(post.createdAt, 'ru')}</time>;
// Опасно — пользователь контролирует datetime атрибут
const userDate = req.query.date; // Из URL или формы
const el = document.createElement('time');
el.setAttribute('datetime', userDate); // Атрибут безопасен
el.textContent = format(userDate, 'ru'); // textContent безопасен
// НО если значение используется как HTML:
container.innerHTML = `<time datetime="${userDate}">${format(userDate, 'ru')}</time>`;
// Опасно: userDate = '"><script>alert(1)</script><time x="'
// Безопасно: создавать элементы программно
const time = document.createElement('time');
time.setAttribute('datetime', userDate); // Безопасно
time.textContent = format(userDate, 'ru'); // Безопасно
container.appendChild(time);
function sanitizeDate(input) {
if (typeof input !== 'string' && typeof input !== 'number' && !(input instanceof Date)) {
return null;
}
const d = new Date(input);
if (isNaN(d.getTime())) return null;
// Ограничить диапазон — отклонить даты из будущего (> 24ч вперёд) и слишком старые
const now = Date.now();
if (d.getTime() > now + 86400_000) return null; // Больше, чем на сутки вперёд
if (d.getTime() < new Date('1970-01-01').getTime()) return null;
return d;
}
function safeFormat(input, locale = 'ru') {
const date = sanitizeDate(input);
if (!date) return '';
return format(date, locale);
}
// Дата из localStorage может быть подменена атакующим
const cachedDate = localStorage.getItem('lastVisit');
const label = format(cachedDate, 'ru'); // Строка из хранилища
// Если label записывается через innerHTML — XSS риск
Валидировать любые данные из внешних источников перед использованием в DOM.
CSP предотвращает выполнение вредоносных скриптов, даже если XSS произошёл:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{RANDOM_NONCE}'
Без unsafe-eval и unsafe-inline
инъектированные скрипты не выполняются.
// На сервере — не включать пользовательские данные напрямую в HTML
const userDate = req.query.date;
const label = safeFormat(userDate, 'ru');
// Безопасно для EJS/Handlebars — автоматическое экранирование
res.render('page', { label }); // <%= label %> — экранируется
В шаблонизаторах использовать синтаксис экранирования
(<%= %> в EJS, {{ }} в Handlebars), а не
сырой вывод (<%- %>, {{{ }}}).
| Проверка | Статус |
|---|---|
textContent вместо innerHTML |
Всегда |
| Валидация дат из внешних источников | Обязательно |
Нет dangerouslySetInnerHTML для дат |
Всегда |
| Программное создание DOM элементов | Рекомендуется |
| CSP заголовки на сервере | Рекомендуется |
| Экранирование в шаблонизаторах | Обязательно |
timeago.js сам по себе безопасен. XSS возникает в коде вокруг него,
когда результат format вставляется через небезопасные
методы (innerHTML, dangerouslySetInnerHTML)
или когда пользовательский ввод попадает в атрибут datetime
через шаблонную строку.