XSS и sanitization

timeago.js форматирует даты в строки и записывает их в DOM через textContent. Это безопасный механизм. Однако уязвимости возникают в коде вокруг библиотеки, а не в ней самой.


Почему textContent безопасен

const el = document.querySelector('time');
el.textContent = '<script>alert(1)</script>'; // Безопасно!

textContent всегда записывает обычный текст — HTML-теги экранируются браузером автоматически. Выводимое значение "<script>alert(1)</script>" будет отображено как текст, а не выполнено.

timeago.js использует именно textContent при render. Прямого XSS через саму библиотеку нет.


Опасный паттерн 1: innerHTML вместо textContent

// Опасно! — 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);

Опасный паттерн 2: React dangerouslySetInnerHTML

// Опасно — если timeAgo содержит пользовательские данные
const timeAgo = format(post.createdAt, 'ru');
return <div dangerouslySetInnerHTML={{ __html: timeAgo }} />;
// Безопасно
return <time>{format(post.createdAt, 'ru')}</time>;

Опасный паттерн 3: Дата из пользовательского ввода

// Опасно — пользователь контролирует 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);
}

Опасный паттерн 4: localStorage или URL параметры как источник даты

// Дата из localStorage может быть подменена атакующим
const cachedDate = localStorage.getItem('lastVisit');
const label      = format(cachedDate, 'ru'); // Строка из хранилища

// Если label записывается через innerHTML — XSS риск

Валидировать любые данные из внешних источников перед использованием в DOM.


Content Security Policy (CSP)

CSP предотвращает выполнение вредоносных скриптов, даже если XSS произошёл:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{RANDOM_NONCE}'

Без unsafe-eval и unsafe-inline инъектированные скрипты не выполняются.


Санитизация для Server-Side Rendering

// На сервере — не включать пользовательские данные напрямую в 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 через шаблонную строку.