Санитизация данных

Санитизация данных — процесс очистки, нормализации и приведения пользовательского ввода к безопасному и предсказуемому виду перед дальнейшей обработкой, отображением или сохранением.

В контексте JavaScript и Intl API санитизация особенно важна при работе с:

  • локалями (locale);
  • валютами;
  • часовыми поясами;
  • единицами измерения;
  • пользовательскими настройками форматирования;
  • данными, поступающими из URL, API или форм.

Ошибки в этих значениях могут привести к:

  • выбросу исключений;
  • некорректному форматированию;
  • различиям в поведении между браузерами;
  • уязвимостям при выводе данных;
  • проблемам интернационализации.

Проверка и нормализация локалей

Проблема необработанных locale-значений

Пользовательский ввод часто содержит:

"EN-us"
"ru_RU"
"de-de "
" english "
null
undefined
123

Intl API ожидает корректные BCP 47 locale-теги:

"en-US"
"ru-RU"
"de-DE"

Некорректный locale способен вызвать ошибку:

new Intl.DateTimeFormat("wrong-locale")

Использование Intl.getCanonicalLocales()

Метод выполняет канонизацию locale-значений.

Intl.getCanonicalLocales("EN-us")

Результат:

["en-US"]

Очистка locale-строки

Базовая санитизация:

function sanitizeLocale(locale) {
    if (typeof locale !== "string") {
        return "en-US";
    }

    const normalized = locale.trim();

    try {
        return Intl.getCanonicalLocales(normalized)[0];
    } catch {
        return "en-US";
    }
}

Использование:

sanitizeLocale(" RU-ru ")

Результат:

"ru-RU"

Поддерживаемые локали

Даже корректный locale может не поддерживаться окружением.

Проверка:

Intl.DateTimeFormat.supportedLocalesOf([
    "ru-RU",
    "fr-FR",
    "invalid"
]);

Результат:

["ru-RU", "fr-FR"]

Фильтрация неподдерживаемых локалей

function getSupportedLocale(locale) {
    const sanitized = sanitizeLocale(locale);

    const supported = Intl.DateTimeFormat
        .supportedLocalesOf([sanitized]);

    return supported[0] || "en-US";
}

Санитизация валют

Опасность произвольных currency-кодов

Intl.NumberFormat требует ISO 4217 валютный код:

new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD"
});

Некорректное значение:

currency: "dollar"

вызовет:

RangeError

Нормализация валютного кода

function sanitizeCurrency(currency) {
    if (typeof currency !== "string") {
        return "USD";
    }

    return currency.trim().toUpperCase();
}

Проверка валидности валюты

Надёжная проверка через Intl.NumberFormat:

function isValidCurrency(currency) {
    try {
        new Intl.NumberFormat("en-US", {
            style: "currency",
            currency
        });

        return true;
    } catch {
        return false;
    }
}

Полная санитизация currency

function sanitizeCurrency(currency) {
    if (typeof currency !== "string") {
        return "USD";
    }

    const normalized = currency
        .trim()
        .toUpperCase();

    if (!isValidCurrency(normalized)) {
        return "USD";
    }

    return normalized;
}

Санитизация часовых поясов

Ошибки при работе с timeZone

Intl.DateTimeFormat принимает только валидные IANA timezone-идентификаторы:

"Europe/Berlin"
"Asia/Tokyo"
"America/New_York"

Некорректное значение:

"moscow"

вызовет исключение.


Проверка часового пояса

function isValidTimeZone(timeZone) {
    try {
        new Intl.DateTimeFormat("en-US", {
            timeZone
        });

        return true;
    } catch {
        return false;
    }
}

Санитизация timezone

function sanitizeTimeZone(timeZone) {
    if (typeof timeZone !== "string") {
        return "UTC";
    }

    const normalized = timeZone.trim();

    if (!isValidTimeZone(normalized)) {
        return "UTC";
    }

    return normalized;
}

Санитизация единиц измерения

Проверка unit в Intl.NumberFormat

Intl.NumberFormat поддерживает ограниченный набор единиц:

"kilometer"
"liter"
"byte"
"celsius"

Некорректное значение:

"kilometers"

приведёт к ошибке.


Безопасная обработка unit

function isValidUnit(unit) {
    try {
        new Intl.NumberFormat("en-US", {
            style: "unit",
            unit
        });

        return true;
    } catch {
        return false;
    }
}

Санитизация unit

function sanitizeUnit(unit) {
    if (typeof unit !== "string") {
        return "kilometer";
    }

    const normalized = unit
        .trim()
        .toLowerCase();

    if (!isValidUnit(normalized)) {
        return "kilometer";
    }

    return normalized;
}

Очистка числовых значений

Проблема строковых чисел

Данные часто приходят в виде строк:

"123"
" 45.6 "
"1,000"
"abc"

Перед передачей в Intl.NumberFormat значения необходимо нормализовать.


Санитизация чисел

function sanitizeNumber(value) {
    if (typeof value === "number") {
        return Number.isFinite(value)
            ? value
            : 0;
    }

    if (typeof value !== "string") {
        return 0;
    }

    const normalized = value
        .trim()
        .replace(/,/g, "");

    const parsed = Number(normalized);

    return Number.isFinite(parsed)
        ? parsed
        : 0;
}

Защита от NaN и Infinity

Intl.NumberFormat способен форматировать:

NaN
Infinity

Однако подобные значения обычно считаются нежелательными.

Проверка:

Number.isFinite(value)

Санитизация дат

Небезопасные date-значения

Проблемные данные:

"invalid-date"
null
undefined
{}
[]

Проверка даты

function isValidDate(date) {
    return (
        date instanceof Date &&
        !Number.isNaN(date.getTime())
    );
}

Санитизация даты

function sanitizeDate(value) {
    const date = new Date(value);

    if (!isValidDate(date)) {
        return new Date();
    }

    return date;
}

Белые списки как основной механизм защиты

Принцип whitelist

Наиболее безопасный подход — разрешать только заранее определённые значения.


Белый список локалей

const ALLOWED_LOCALES = [
    "en-US",
    "ru-RU",
    "de-DE"
];

function sanitizeLocale(locale) {
    try {
        const normalized =
            Intl.getCanonicalLocales(locale)[0];

        if (ALLOWED_LOCALES.includes(normalized)) {
            return normalized;
        }
    } catch {}

    return "en-US";
}

Белый список валют

const ALLOWED_CURRENCIES = [
    "USD",
    "EUR",
    "KZT",
    "RUB"
];

function sanitizeCurrency(currency) {
    const normalized = String(currency)
        .trim()
        .toUpperCase();

    return ALLOWED_CURRENCIES.includes(normalized)
        ? normalized
        : "USD";
}

Санитизация пользовательских options-объектов

Риск передачи произвольных настроек

Опасная практика:

new Intl.NumberFormat(locale, userOptions)

Пользователь способен передать:

{
    style: "currency",
    currency: "INVALID",
    maximumFractionDigits: 999
}

Извлечение только разрешённых полей

function sanitizeNumberFormatOptions(options = {}) {
    return {
        style: sanitizeStyle(options.style),

        currency: sanitizeCurrency(
            options.currency
        ),

        maximumFractionDigits:
            sanitizeFractionDigits(
                options.maximumFractionDigits
            )
    };
}

Санитизация style

const ALLOWED_STYLES = [
    "decimal",
    "currency",
    "percent",
    "unit"
];

function sanitizeStyle(style) {
    return ALLOWED_STYLES.includes(style)
        ? style
        : "decimal";
}

Ограничение диапазонов

function sanitizeFractionDigits(value) {
    const number = Number(value);

    if (!Number.isInteger(number)) {
        return 2;
    }

    return Math.min(
        Math.max(number, 0),
        20
    );
}

Защита от prototype pollution

Потенциальная проблема

Если без проверки копировать пользовательский объект:

{
    "__proto__": {
        hacked: true
    }
}

возможно загрязнение прототипа.


Безопасное копирование

function safeObject(input) {
    const result = Object.create(null);

    for (const key of Object.keys(input)) {
        result[key] = input[key];
    }

    return result;
}

Санитизация перед сериализацией

Очистка данных перед JSON

Перед отправкой на сервер:

const payload = {
    locale: sanitizeLocale(user.locale),
    currency: sanitizeCurrency(user.currency),
    timeZone: sanitizeTimeZone(user.timeZone)
};

Исключение undefined-значений

function removeUndefined(obj) {
    return Object.fromEntries(
        Object.entries(obj)
            .filter(([_, value]) =>
                value !== undefined
            )
    );
}

Санитизация массивов

Проблемы пользовательских коллекций

Например:

[
    "en-US",
    "invalid",
    null,
    "ru-RU"
]

Очистка массива локалей

function sanitizeLocales(locales) {
    if (!Array.isArray(locales)) {
        return ["en-US"];
    }

    return locales
        .map(sanitizeLocale)
        .filter(Boolean);
}

Санитизация URL-параметров

Данные из query string

Пример URL:

?locale=ru-RU&currency=kzt

Безопасное извлечение параметров

const params = new URLSearchParams(
    location.search
);

const locale = sanitizeLocale(
    params.get("locale")
);

const currency = sanitizeCurrency(
    params.get("currency")
);

Безопасные fallback-значения

Почему fallback обязателен

Любая санитизация должна возвращать предсказуемый результат.

Плохой вариант:

return null;

Хороший вариант:

return "en-US";

Типичные fallback-значения

Тип Fallback
locale "en-US"
currency "USD"
timeZone "UTC"
number 0
date new Date()
unit "kilometer"

Комплексная санитизация Intl-конфигурации

Универсальная функция

function sanitizeIntlConfig(config = {}) {
    return {
        locale: sanitizeLocale(
            config.locale
        ),

        currency: sanitizeCurrency(
            config.currency
        ),

        timeZone: sanitizeTimeZone(
            config.timeZone
        ),

        unit: sanitizeUnit(
            config.unit
        )
    };
}

Практический пример безопасного форматирования

Форматирование валюты

function formatPrice(value, config) {
    const safeConfig =
        sanitizeIntlConfig(config);

    const amount =
        sanitizeNumber(value);

    return new Intl.NumberFormat(
        safeConfig.locale,
        {
            style: "currency",
            currency: safeConfig.currency
        }
    ).format(amount);
}

Использование:

formatPrice("1,200.5", {
    locale: " RU-ru ",
    currency: "kzt"
});

Результат:

1 200,50 ₸

Обработка ошибок Intl API

Централизованная защита

function safeIntl(callback, fallback) {
    try {
        return callback();
    } catch {
        return fallback;
    }
}

Пример использования

const result = safeIntl(
    () => {
        return new Intl.DateTimeFormat(
            locale,
            options
        ).format(date);
    },

    "Invalid format"
);

Иммутабельность при санитизации

Почему нельзя изменять исходные данные

Плохая практика:

options.currency = "USD";

Лучше:

return {
    ...options,
    currency: "USD"
};

Архитектура sanitization layer

Выделенный слой очистки

Типичная структура:

src/
├── intl/
│   ├── sanitizeLocale.js
│   ├── sanitizeCurrency.js
│   ├── sanitizeTimeZone.js
│   └── sanitizeOptions.js

Централизованная точка входа

export function sanitizeIntl(data) {
    return {
        locale: sanitizeLocale(data.locale),
        currency: sanitizeCurrency(data.currency),
        timeZone: sanitizeTimeZone(data.timeZone)
    };
}

Производительность санитизации

Избежание повторных проверок

Плохо:

sanitizeLocale(locale)
sanitizeLocale(locale)
sanitizeLocale(locale)

Лучше:

const safeLocale =
    sanitizeLocale(locale);

Кэширование валидаторов

const localeCache = new Map();

function cachedLocale(locale) {
    if (localeCache.has(locale)) {
        return localeCache.get(locale);
    }

    const sanitized =
        sanitizeLocale(locale);

    localeCache.set(
        locale,
        sanitized
    );

    return sanitized;
}

Тестирование санитизации

Набор обязательных тестов

Проверяются:

  • пустые строки;
  • null;
  • undefined;
  • неверные locale;
  • неверные валюты;
  • массивы;
  • объекты;
  • числа;
  • Unicode-символы;
  • XSS-подобные строки.

Пример теста

test("invalid locale fallback", () => {
    expect(
        sanitizeLocale("invalid")
    ).toBe("en-US");
});

Типичные ошибки

Отсутствие try/catch

Ошибка:

new Intl.NumberFormat(locale, options)

без обработки исключений.


Доверие пользовательскому вводу

Ошибка:

currency: userCurrency

без валидации.


Использование blacklist вместо whitelist

Плохо:

if (currency !== "BAD")

Хорошо:

ALLOWED_CURRENCIES.includes(currency)

Отсутствие fallback

Ошибка:

return undefined;

Практика defensive programming

Никогда не доверять входным данным

Любое значение считается потенциально некорректным:

function format(value, locale) {
    const safeLocale =
        sanitizeLocale(locale);

    const safeValue =
        sanitizeNumber(value);

    return new Intl.NumberFormat(
        safeLocale
    ).format(safeValue);
}

Рекомендации по промышленной разработке

Основные принципы

  • вся пользовательская конфигурация проходит через слой санитизации;
  • используются whitelist-подходы;
  • применяются fallback-значения;
  • выполняется строгая проверка типов;
  • все вызовы Intl API защищаются от исключений;
  • sanitization-логика хранится централизованно;
  • значения нормализуются до единого формата;
  • любые внешние данные считаются недоверенными;
  • форматирование никогда не выполняется напрямую над сырыми данными;
  • sanitization-функции должны быть чистыми и предсказуемыми.