JSON — это инструмент, которым каждый веб-разработчик пользуется ежедневно. На первый взгляд кажется, что JSON слишком прост и незамысловат, чтобы читать про него целую статью. Но если вы не знаете, что такое санитизация данных, никогда не слышали про JSON Injection или XSS атаки, если информация о том, что JSON.parse и JSON.stringify принимают 2 и 3 аргумента для вас новость, то эта статья будет вам полезна.

Общие положения

JSON (JavaScript Object Notation) — это легковесный формат данных, который используется для обмена данными между клиентом и сервером в веб-приложениях.

В начале 2000-х годов была потребность в формате данных, который был бы удобнее и проще XML. Дуглас Крокфорд предложил использовать для обмена данными формат объектного литерала JavaScript, так и появился JSON. Предложенный Крокфордом формат оказался более компактным по сравнению с XML, проще для чтения и записи и прост в парсинге и генерации. В результате сейчас JSON является основным форматом обмена данными в веб-разработке.

JSON основан на двух основных структурах данных: объекте и массиве. Объект — это набор пар "ключ-значение", массив — это упорядоченный список значений. Ключи в объектах JSON всегда являются строками, а значения могут быть строками, числами, объектами, массивами, булевыми значениями или null.

{
  "name": "John",
  "age": 30,
  "is_student": false,
  "courses": ["math", "history", "chemistry"],
  "personal_info": {
    "address": "123 Main St",
    "phone_number": "123-456-7890"
  }
}

Несмотря на простоту и удобство, работа с JSON может быть связана с рядом проблем безопасности, если не подходить к этому процессу обдуманно.

Безопасное использование JSON

Основные виды атак

1. JSON Injection

Эта атака заключается в том, что злоумышленник вставляет вредоносный код в JSON, который затем обрабатывается веб-приложением.

Пример: Если веб-приложение берет пользовательский ввод и непосредственно вставляет его в JSON без проверки или санитизации (об этом подробно дальше), злоумышленник может вставить вредоносный код. Этот код может быть выполнен при обработке JSON.

const userInput = getUserInput();
const data = `{"name": "${userInput}"}`
const jsonData = JSON.parse(data);

В этом случае, если пользователь вводит что-то вроде "", "isAdmin": "true"}, вредоносный код будет добавлен в JSON, создавая объект { "name": "", "isAdmin": "true"}.

2. Cross-Site Scripting (XSS)

XSS — это тип атаки, при которой злоумышленник вставляет вредоносный скрипт в веб-страницу, который затем выполняется в браузере пользователя.

Пример: Если веб-приложение вставляет данные JSON напрямую в HTML без предварительной санитизации, это может привести к XSS-атаке.

const jsonData = getJsonData();
const element = document.getElementById('output');
element.innerHTML = jsonData.content;

Если jsonData.content содержит что-то вроде

<script>maliciousCode()</script>

вредоносный код будет выполнен в браузере пользователя.

3. SQL Injection и JSON

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

Примером может служить ситуация, когда данные с клиентской стороны, например, из формы на веб-странице, принимаются в формате JSON, и затем используются для построения SQL-запроса. 

const userData = JSON.parse(userInput);
const query = `SELECT * FROM users WHERE name = '${userData.name}' AND password = '${userData.password}'`;

Злоумышленник может подставить в поле name или password строку, которая, будучи вставлена в запрос SQL, изменит его логику. Например, следующий JSON: 

{
  "name": "' OR '1'='1'; --",
  "password": "any"
}

после подстановки в запрос приведет к его изменению:

SELECT * FROM users WHERE name = '' OR '1'='1'; --' AND password = 'any'

В данном случае, поскольку условие '1'='1' всегда истинно, это приведет к выбору всех пользователей и злоумышленник сможет обойти проверку пароля и войти в систему под любым аккаунтом.

Дисклеймер: я привел стандартный пример SQL-инъекции, он призван демонстрировать концепцию, а не реальный вектор атаки.

Важно понимать, что JSON сам по себе не имеет отношения к SQL-инъекциям. Однако, если данные, полученные в формате JSON, используются для формирования SQL-запросов без предварительной валидации и санитизации, то это может создать уязвимости для таких атак.

Чтобы предотвратить SQL-инъекции, рекомендуется использовать подготовленные или параметризованные запросы, а также проводить валидацию и санитизацию всех входных данных. Подготовленные запросы используют параметры вместо прямой вставки ввода пользователя в запрос. Библиотеки баз данных обычно предоставляют функции для создания подготовленных запросов. 

Пример использования подготовленного запроса на Node.js с использованием MySQL:

const userData = JSON.parse(userInput);
let sql = `SELECT * FROM users WHERE name = ? AND password = ?`;
const inserts = [userData.name, userData.password];
sql = mysql.format(sql, inserts);

В этом примере, даже если злоумышленник попытается внедрить вредоносный код через JSON, он будет безопасно обработан библиотекой MySQL, и SQL-инъекция не произойдет.

Предотвращение атак

Основой безопасного использования JSON являются валидация и санитизация данных. Эти инструменты универсальны и эффективны против всех описанных выше видов атак.

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

Санитизация данных — это процесс очистки данных от вредоносных или нежелательных элементов. В контексте веб-разработки это обычно включает удаление или замену символов, которые могут быть использованы для проведения атак, таких как вставка SQL, XSS или JSON Injection. 

Инструменты для санитизации данных:

  1. DOMPurify: Это библиотека для JavaScript, которая позволяет очищать HTML, MathML и SVG от XSS атак. DOMPurify очень прост в использовании. Просто передайте ей строку, которую нужно санитизировать, и она вернет безопасную строку.

const clean = DOMPurify.sanitize(dirty);
  1. express-validator: Это набор middleware функций для Express.js, который предоставляет мощные инструменты валидации и санитизации. 

const express = require('express');
const { query, validationResult } = require('express-validator');

const app = express();
app.use(express.json());
app.get('/hello', query('person').notEmpty().escape(), (req, res) => {
  const result = validationResult(req);
  if (result.isEmpty()) {
    return res.send(`Hello, ${req.query.person}!`);
  }
  res.send({ errors: result.array() });
});
app.listen(3000);

В этом примере ‘express-validator’ проверяет, что имя пользователя не пустое, а вызов escape() производит санитизацию данных.

Валидация данных

Валидация — это процесс проверки данных на соответствие определенным критериям или правилам. В контексте JSON валидация обычно включает в себя проверку следующего:

  • Соответствие структуры и типов данных предполагаемым. Например, если ожидается объект с определенными полями, валидация должна убедиться, что все эти поля присутствуют и имеют правильные типы данных.

  • Отсутствие нежелательных символов или паттернов, которые могут вызвать проблемы при обработке данных (например, вредоносный код).

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

JavaScript предоставляет некоторые базовые функции для валидации JSON.

JSON.stringify

JSON.stringify() преобразует объект JavaScript в строку JSON. Этот метод принимает три аргумента: значение для преобразования, функцию-заменитель и пробелы.

  1. Значение: это может быть любой объект JavaScript или значение, которое вы хотите преобразовать в JSON.

  2. Функция-заменитель (replacer): это опциональная функция, которая преобразует значения объекта перед их сериализацией. Она может использоваться для фильтрации или преобразования значений.

  3. Пробелы: опциональный параметр, который контролирует отступы в сериализованном JSON. Это может быть число (количество пробелов) или строка (до 10 символов, используемых в качестве пробела).

const json = JSON.stringify(obj, (key, value) => {
  if (key === 'id') return undefined;
  return value;
}, 2);

В этом примере значение "id" удаляется из сериализованного JSON и используются два пробела для отступов в JSON.

Сериализация — это процесс преобразования состояния объекта в формат, который может быть сохранен или передан и затем восстановлен. В контексте JSON, сериализация обычно включает преобразование объекта JavaScript в строку JSON.

Десериализация — это обратный процесс. Он преобразует сериализованные данные обратно в их первоначальную форму. В контексте JSON, десериализация обычно включает преобразование строки JSON обратно в объект JavaScript.

JSON.parse

JSON.parse() преобразует строку JSON обратно в объект JavaScript. Этот метод принимает два аргумента: строку для преобразования и функцию-ревайвер.

  1. Строка: это должна быть корректная строка JSON, которую вы хотите преобразовать в объект JavaScript.

  2. Функция-ревайвер (reviver): это опциональная функция, которая преобразует значения объекта после их десериализации. Она может использоваться для преобразования или восстановления определенных значений.

const obj = JSON.parse(str, (key, value) => {
  if (key === 'date') return new Date(value);
  return value;
});

В примере выше, функция-ревайвер преобразует строку в объект Date в процессе десериализации JSON.

JSON Schema

Для более сложных случаев валидации, таких как проверка структуры объекта JSON и типов данных, может потребоваться использование сторонних библиотек, таких как Ajv (Another JSON Schema Validator).

Ajv использует стандарт, который называется JSON Schema который нужен для описания и валидации структуры JSON-данных.

Пример JSON Schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Product",
  "type": "object",
  "properties": {
    "id": {
      "type": "number"
    },
    "name": {
      "type": "string"
    },
    "price": {
      "type": "number",
      "exclusiveMinimum": 0
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "uniqueItems": true
    }
  },
  "required": ["id", "name", "price"]
}

Обратите внимание на поле:

"$schema": "http://json-schema.org/draft-07/schema#",

"$schema" является особым полем в JSON Schema. Это URI, который указывает на версию JSON Schema, которая была использована для создания вашей схемы. Это необходимо для корректной интерпретации и валидации схемы.

Вышеуказанная схема описывает объект "Product" с полями "id", "name", "price" и "tags". Поля "id", "name" и "price" являются обязательными. Поле "tags" должно быть массивом уникальных строк.

В данном примере, "$schema": "http://json-schema.org/draft-07/schema#" указывает, что ваша схема написана с использованием версии "draft-07" JSON Schema. Это помогает программам, которые обрабатывают вашу схему, правильно интерпретировать и применять правила валидации.

Ajv позволяет определить схему для данных, а затем проверять объекты на соответствие этой схеме.

const Ajv = require("ajv")
const ajv = new Ajv()
const schema = {
  type: "object",
  properties: {
    foo: {type: "integer"},
    bar: {type: "string"}
  },
  required: ["foo","bar"],
  additionalProperties: false
}
const data = {foo: 1, bar: "abc"}
const valid = ajv.validate(schema, data)
if (!valid) console.log(ajv.errors)

В этом примере, если данные не соответствуют схеме (например, foo не является числом, или одно из полей отсутствует), Ajv вернет ошибки, которые затем можно обработать.

Общие ошибки при работе с JSON и их решения

Синтаксические ошибки, ошибки сериализации/десериализации и ошибки передачи данных — это некоторые из наиболее распространенных проблем, с которыми разработчики сталкиваются при работе с JSON.

  1. Синтаксические ошибки: JSON имеет строгий синтаксис. Например, все ключи должны быть заключены в двойные кавычки, а не в одинарные. 

    { 'key': "value" } // ошибка
    { "key": "value" } // правильно

    Кроме того, каждый элемент в массиве или пара ключ-значение в объекте должны быть разделены запятыми. 

    { "key1": "value1" "key2": "value2" } // ошибка
    { "key1": "value1", "key2": "value2" } // правильно

    Синтаксические ошибки могут быть легко обнаружены с помощью JSON валидаторов, например, JSONLint.

  2. Ошибки сериализации/десериализации: Некоторые объекты JavaScript не могут быть корректно сериализованы в JSON или десериализованы из JSON. Например, функции и объекты Date в JavaScript не могут быть преобразованы в JSON. В этих случаях необходимо преобразовать объекты в поддерживаемые типы данных перед сериализацией или после десериализации. В этом примере, при преобразовании JSON обратно в объект, функция ревайвер преобразует строку даты обратно в объект Date. Без функции ревайвер, дата осталась бы строкой.

    const user = {
      name: 'Tom',
      registered: new Date(),
    };
    const json = JSON.stringify(user);
    // Преобразуем JSON обратно в объект, восстанавливая дату с помощью функции ревайвер
    const parsedUser = JSON.parse(json, (key, value) => {
      if (key == 'registered') return new Date(value);
      return value;
    });
    console.log(parsedUser.registered); // Выведет объект Date, а не строку
  3. Ошибки при передаче данных: JSON часто используется для передачи данных между клиентом и сервером. Если эти данные не правильно кодируются или декодируются, или если они теряются в процессе передачи, это может привести к ошибкам. Обычно ошибки кодирования и декодирования связаны с ошибками в заголовке ‘Content-Type’, там необходимо указать значение ‘application/json’.

'Content-Type': 'text/plain' // ошибка 
'Content-Type': 'application/json' // правильно

Заключение

Правильное использование JSON в веб-разработке критически важно для обеспечения безопасности и эффективности приложения. Валидация и санитизация данных, использование безопасных функций для обработки JSON и избегание распространенных ошибок могут помочь обеспечить надежную и безопасную работу с JSON. Надеюсь теперь вы согласитесь, что даже в таком простом инструменте, как JSON, могут быть неожиданные подводные камни и особенности, о которых нужно знать.

Также напоследок хочу пригласить вас на бесплатный урок, на котором поговорим о том, как работать с многопоточностью в NodeJS. Также на уроке вы познакомитесь с WorkerThreads. Регистрация доступна по ссылке.

Полезные ссылки

Что еще почитать по теме:

  1. Документация MDN по JSON

  2. Руководство OWASP по предотвращению SQL инъекций.

  3. Подробнее о истории и важности JSON можно прочитать здесь и здесь.

  4. Подробнее про JSON Schema тут и тут.

Инструменты:

  1. Ajv (Another JSON Schema Validator): Один из самых популярных JSON Schema валидаторов. Он позволяет создавать сложные правила валидации, используя JSON Schema стандарт.

  2. DOMPurify: Библиотека для санитизации HTML, которая помогает предотвращать XSS атаки.

  3. express-validator: Библиотека для валидации и санитизации данных в Express.js приложениях.

  4. Joi: Мощная библиотека для валидации данных в JavaScript. Она поддерживает множество типов данных и позволяет создавать сложные правила валидации.

  5. validator.js: Библиотека строковой валидации и санитизации.

  6. js-xss: Безопасный и мощный модуль санитизации HTML для JavaScript, предназначенный для предотвращения XSS атак.

  7. sanitize-html: Библиотека, которая позволяет обрабатывать HTML, сохраняя только те элементы и атрибуты, которые вы хотите разрешить.

Комментарии (6)


  1. dopusteam
    07.06.2023 07:40

    В этом случае, если пользователь вводит что-то вроде "", "isAdmin": "true"}, вредоносный код будет добавлен в JSON, создавая объект { "name": "", "isAdmin": "true"}.

    А что в этом плохого и где тут вредоносный код?

    И, кстати, вы проверяли пример? Я вот проверил и получил невалидный JSON который на JSON.parse развалился

    const userInput = `"", "isAdmin": "true"}`;
    const data = `{"name": "${userInput}"}`
    console.log(data)
    // {"name": """, "isAdmin": "true"}"}

    SQL injection и xss тоже не особо с json связаны, кстати


    1. NiPh
      07.06.2023 07:40
      +2

      Возможно речь о каких-то исключительных ситуациях когда пользовательский ввод превращается в json уже на бэкенде и передается куда-то дальше без проверок. Либо если пользователь имеет терминал ввода без режима отладки в безопасном окружении, где не может сам отправлять произвольные запросы, но таким образом может модифицировать поля формы и опять же, они без проверки полетят дальше. Но мне кажется что если в системе присутствует такое доверие к пользовательскому вводу, то скорее всего json инъекции будут не самой большой проблемой.


    1. AdelIbragimov
      07.06.2023 07:40
      -2

      Любой пользователь может стать админом


      1. dopusteam
        07.06.2023 07:40

        Ваш сарказм тут не все поймут


  1. mayorovp
    07.06.2023 07:40
    +1

    Санитизацией данных следует заниматься только "от безнадёги". Валидация нужна, но не всегда помогает.


    Самая правильная защита — автоматическое экранирование при выводе.


    К примеру, JSON нужно формировать не через форматирование строк, а через JSON.stringify или аналог.


    SQL запрос нужно делать параметризованным.


    Для HTML разметки тоже есть нормальные генераторы.


  1. shasoftX
    07.06.2023 07:40

    В этом случае, если пользователь вводит что-то вроде "", "isAdmin": "true"}

    1. Логичнее вводить "", "isAdmin": true, чтобы это было как bool, а не как строка

    2. const data = `{"name": "${userInput}"}` - тут уж вообще, на мой взгляд, перебор идиотизма потому что

      const data = `{"name": userInput}` выглядит более логично + безопасно