Привет! это примечания к видео Учебное пособие по уязвимостям безопасности JavaScript — с примерами кода от freeCodeCamp.org. Надеюсь, это поможет вам научиться быстрее!

Структура статьи:

  1. Проверка URL
  2. Обработка запросов
  3. Проверка токена
  4. Проверка токена
  5. NoSQL-инъекция
  6. Обработка регистрации

Проверка URL

Так что у вас может быть такой код.

function useQuery() {
  const { search } = useLocation();
  return React.useMemo(() => new URLSearchParams(search), [search]);
}

function QueryParamsDemo() {
  let query = useQuery();
  return <div>
    <h1>Return home?</h2>
    <a href={query.get("redirect")}>click to go home!</a>
  </div>
}

На первый взгляд этот код может показаться безобидным — простая реализация перенаправления домашней ссылки с использованием параметра запроса под названием «перенаправление».

выглядит хорошо, правда?

no!!!

Многие разработчики не знают, что этот код на самом деле является типичным примером уязвимого компонента JavaScript с уязвимостью межсайтового скриптинга (XSS).

Представьте себе злоумышленника, который может создать вредоносный URL-адрес следующим образом: https://example.com/settings?redirect=javascript://doSomethingbad()

(Необязательно) В этом случае, если пользователь щелкнет предоставленную ссылку, код JavaScript, указанный в параметре запроса redirect (doSomethingbad()), будет выполнен непосредственно в его браузере. Это создает серьезную угрозу безопасности, позволяя злоумышленнику выполнять несанкционированные действия, красть конфиденциальные данные или ставить под угрозу работу пользователя в Интернете.

Чтобы устранить эту уязвимость и защитить наше приложение и пользователей от потенциальных XSS-атак, нам необходимо реализовать надлежащую проверку URL-адресов. Давайте представим функцию validateURL, которая проверяет, имеет ли URL действительный протокол https::

  1. Подтвердить URL-адрес
function validateURL(url) {
  try {
    const userSuppliedUrl = new URL(url);

    if (userSuppliedUrl.protocol === "https:") {
      return url;
    }
  } catch (error) {
    // Handle any potential parsing errors
  }

  return "/";
}
...

return (
  <div>
    <h1>Return home?</h2>
    <a href={validateURL(query.get("redirect"))}>click to go home!</a>
  </div>
);

Обработка запросов

Вот код, обрабатывающий запрос на получение с URL-адресом запроса для получения данных:

app.get("/api/data", async (req, res) => {

  const url = req.query.url;

  try {
    const response = await fetch(url);
    const data = await response.json();
    
    res.status(200).json({ data: data });
  } catch(err) {
    console.log(err)
    res.status(500).json({ err: err.msg });
  }
}

Приведенный выше код извлекает параметр url непосредственно из запроса без какой-либо проверки. Это делает сервер уязвимым для потенциальных SSRF-атак , когда злоумышленник может манипулировать параметром url для доступа к конфиденциальным внутренним ресурсам или выполнения произвольного кода.

Итак, нам нужно внедрить проверку URL и добавление в белый список:

const allowedURLs = ["https://internal.myapp.com/api/data/i-am-allowed/"];

app.get("/api/data", async (req, res) => {
  const url = req.query.url;

  if (!allowedURLs.includes(url)) {
    return res.status(403).json({ error: "Invalid URL" });
  }

  try {
    const response = await fetch(url);
    const data = await response.json();
    
    res.status(200).json({ data: data });
  } catch(err) {
    console.log(err)
    res.status(500).json({ error: err.msg });
  }
});

В расширенном коде мы вводим массив allowedURLs, содержащий белый список разрешенных URL-адресов. Затем мы проверяем параметр url, проверяя, существует ли он в массиве allowedURLs. Если URL-адрес отсутствует в белом списке, возвращается ответ 403 Forbidden, чтобы отклонить запрос.

Проверка токена

Это функция, реализующая проверку токенов:

export function checkToken(userSupplied) {
  
  const account = account.retrieveToken(userSupplied)
  if(account) {
    if(account.service.token === user.service.token) {
      return true;
    }
  }
  return false;
}

Приведенный выше код сравнивает предоставленный пользователем токен с сервисным токеном учетной записи. Однако он упускает из виду потенциальную уязвимость атаки по времени.

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

В уязвимом коде время выполнения инструкции if зависит от сравнения токенов. Злоумышленник может воспользоваться этим, предоставив токены и проанализировав разницу во времени, скомпрометировав процесс проверки.

Например:

// actual token: "abc"
// test1 "def" -> takes 0ms -> okayyy
// test2 "ghi" -> takes 0ms -> hmm...
// test2 "aaa" -> takes 1ms -> something found here!

Вот как мы это исправляем:

import crypto from "crypto"

export function checkToken(userSupplied) {
  
  const account = account.retrieveToken(userSupplied)
  if(account) {
    if(crypto.timingSafeEqual(account.service.token, user.service.token)) {
      return true;
    }
  }
  return false;
}

Мы можем просто использовать timingSafeEqual() из пакета crypto.

Проверка токена

Вот функция, реализующая проверку токена:

const SOMEOBJECT = {}

app.get("/validateToken", (req, res) => {
  if(req.header('token')) {
    const token = Buffer.from(req.header('token'), 'base64')

    if(SOMEOBJECT[token] && token) {
      return res.send("true")
    }
  }
})

В приведенном выше коде есть уязвимость, которую можно использовать, указав определенное значение для token. Когда для token установлено значение "__proto__", это может обойти проверку SOMEOBJECT[token] и привести к непредвиденному поведению.

Чтобы устранить эту уязвимость, нам нужно убедиться, что значение token не имеет доступа к непредусмотренным свойствам. Вот улучшенный код:

// Code with vulnerability fixed
const SOMEOBJECT = {};

app.get("/validateToken", (req, res) => {
  const token = req.header('token');

  if (token && Buffer.from(token, 'base64')) {
    if (Object.prototype.hasOwnProperty.call(SOMEOBJECT, token)) {
      return res.send("true");
    } else {
      return res.send("false");
    }
  } else {
    return res.status(400).send("Invalid token");
  }
});

NoSQL-инъекция

Внедрение NoSQL атакует целевые приложения, использующие базы данных NoSQL, используя уязвимости в обработке пользовательского ввода. Подобно атакам с внедрением SQL, злоумышленники манипулируют или внедряют вредоносный код в запросы к базе данных, используя неправильную проверку ввода и построение запросов.

Рассмотрим следующий уязвимый фрагмент кода:

const userId = req.body.userId;
const query = { $where: `this.userId == '${userId}'` };

db.collection('users').find(query, (err, result) => {
  if (err) {
    // Handle error
  } else {
    // Process result
  }
});

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

userId: ' || this.password || '

Когда злоумышленник предоставляет указанную выше полезную нагрузку, результирующий запрос становится таким:

{ $where: `this.userId == '' || this.password || ''` }

В этом случае злоумышленник пытается получить поле password из базы данных, внедрив дополнительный код через параметр userId. Если запрос выполняется без надлежащей проверки входных данных или построения запроса, он может возвращать непреднамеренные результаты, что может привести к раскрытию конфиденциальной информации.

Сегодня мы не будем вдаваться в подробности об этом. Вам лучше убедиться в этом самим!

Обработка регистрации

Ниже приведена типичная концепция, когда ваш код обрабатывает запрос на регистрацию.

у вас может быть такой код:

if(db.users.find(...),(err, res) => {
   if(err) {
   // handle err...
  } else {
    await db.users.insert(req.body) // -> bad!!
  }
})

и это довольно плохо, потому что злоумышленник может легко отправить тело запроса с чем-то вроде получения прав администратора:

const body = 
{
  username: "...",
  password: "...",
  isAdmin: true //-> oh no!
}

да, в общем, вам просто нужно вставить пользователя с параметрами, которые вы действительно хотите:

db.users.insert({username: req.body.username, password: encrypt(req.body.password)})

Хорошо, это так! Надеюсь, вы хорошо учились! Не забывайте хлопать🥰~

кредит: Учебное пособие по уязвимостям безопасности JavaScript — с примерами кода