Защита от prototype pollution

Prototype pollution — атака, при которой злоумышленник модифицирует Object.prototype или Array.prototype, добавляя свойства, которые наследуют все объекты. Применительно к timeago.js риск невысок, но защита важна при обработке недоверенных данных.


Что такое prototype pollution

// Атакующий добавляет свойство в прототип
const malicious = JSON.parse('{"__proto__": {"polluted": true}}');

const innocent = {};
console.log(innocent.polluted); // true — объект "заражён"

Если такой объект попадает в конфигурацию timeago.js или в данные, передаваемые в format, это может привести к неожиданному поведению.


Риски для timeago.js

timeago.js не обрабатывает произвольные объекты конфигурации из внешних источников. Основной риск — в коде, который:

  1. Принимает настройки (locale, fallback) из пользовательского ввода или URL.
  2. Использует Object.assign или spread с данными из API.
  3. Парсит JSON без валидации.

Безопасный парсинг конфигурации

// Небезопасно — прямое слияние из внешнего источника
const userConfig = JSON.parse(req.body.config);
const config = { ...defaultConfig, ...userConfig }; // Может быть polluted
// Безопасно — белый список ключей
function sanitizeConfig(raw) {
  const allowed = ['locale', 'fallback', 'updateInterval'];
  const config  = Object.create(null); // Без прототипа

  allowed.forEach(key => {
    if (Object.prototype.hasOwnProperty.call(raw, key)) {
      config[key] = raw[key];
    }
  });

  return config;
}

Object.create(null) для внутренних словарей

// Небезопасно — словарь с прототипом
const localeMap = {};

// Безопасно — словарь без прототипа
const localeMap = Object.create(null);
localeMap['ru'] = ruLocaleFunc;
localeMap['de'] = deLocaleFunc;

// Проверка
'ru' in localeMap;  // true
'toString' in localeMap; // false — нет унаследованных свойств

hasOwnProperty вместо in

// Небезопасно — проверяет прототип
if ('locale' in config) { /* ... */ }

// Безопасно — только собственные свойства
if (Object.prototype.hasOwnProperty.call(config, 'locale')) { /* ... */ }
// Или в ES2022:
if (Object.hasOwn(config, 'locale')) { /* ... */ }

Защита при создании кастомного реестра локалей

class SafeLocaleRegistry {
  // Map не подвержена prototype pollution
  private locales = new Map<string, LocaleFunc>();

  register(code: string, fn: LocaleFunc): void {
    // Дополнительная проверка: только безопасные ключи
    if (!/^[a-zA-Z_]{2,10}$/.test(code)) {
      throw new TypeError(`Invalid locale code: "${code}"`);
    }
    this.locales.set(code, fn);
  }

  get(code: string): LocaleFunc | undefined {
    return this.locales.get(code);
  }
}

Map не наследует от Object.prototype — она не подвержена prototype pollution.


Заморозка конфигурации

const CONFIG = Object.freeze({
  locale:         'ru',
  updateInterval: 60_000,
  batchSize:      50,
});

// CONFIG.locale = 'de'; // TypeError в strict mode

Замороженный объект нельзя изменить даже через pollution.


Валидация locale перед использованием

const SAFE_LOCALES = new Set(['ru', 'en_US', 'de', 'fr', 'zh_CN', 'ja', 'ko']);

function safeFormat(date, locale) {
  // Только известные значения
  const safeLocale = SAFE_LOCALES.has(locale) ? locale : 'ru';
  return format(date, safeLocale);
}

Библиотека защиты: sjson

npm install @fastify/secure-json-parse
import { parse } from '@fastify/secure-json-parse';

// Парсинг без опасности pollution
const data = parse('{"__proto__": {"polluted": true}}');
console.log({}.polluted); // undefined — защита сработала

Аудит зависимостей

npm audit

Следить за moderate и critical уязвимостями в зависимостях timeago.js:

npm audit --audit-level=high

Чеклист защиты от prototype pollution

Мера Применение
Object.create(null) для словарей Внутренние карты locale, конфиги
Map вместо объектов Реестры, кеши
Object.hasOwn вместо in Проверка наличия свойств
Белый список ключей конфига При слиянии с внешними данными
Object.freeze для констант Неизменяемые конфиги
Валидация locale по whitelist Перед передачей в timeago.js
@fastify/secure-json-parse Парсинг JSON из недоверенных источников