XMLHttpRequest и кросс-доменные запросы

XMLHttpRequest и кросс-доменные запросы

Здравствуйте! Обычно запрос XMLHttpRequest может делать запрос только на текущий сайт. При попытке использовать иной домен – браузер выдаёт ошибку. Делается  это из соображений  безопасности,  чтобы не  было  возможности провести атаку типа XSS взлом.

Существует и современный стандарт XMLHttpRequest, он ещё правда в состоянии черновика, но предусматривает кросс-доменные запросы и многое другое.

Большинство возможностей этого стандарта уже поддерживаются всеми браузерами.

кроссдоменные запросы xmlhttprequest

Кросс-доменные запросы

Давайте рассмотрим  кросс-доменные запросы на примере кода:

// (1) 
var XHR = ("onload" in new XMLHttpRequest()) ? XMLHttpRequest : XDomainRequest; 
var xhr = new XHR(); 
// (2) запрос на другой домен :) 
xhr.open('GET', 'http://anywhere.com/request', true); 
xhr.onload = function() { alert( this.responseText ); } 
xhr.onerror = function() { alert( 'Ошибка ' + this.status ); } 
xhr.send();
  1. Мы создаём XMLHttpRequest и проверяем, поддерживает ли он событие onload. Если нет, то это старый XMLHttpRequest, значит это IE8,9, и надо использовать XDomainRequest.
  2. Запрос на другой домен отсылается просто указанием соответствующего URL в open. Он обязательно должен быть асинхронным, в остальном – никаких особенностей.

Контроль безопасности

Кросс-доменные запросы проходят специальный контроль безопасности, цель которого – не дать взломать сайт.

Давайте, вообразим, что появился стандарт, который даёт, без ограничений, возможность делать любой странице HTTP-запросы куда угодно и какие угодно.

Как сможет этим воспользоваться злой взломщик?

Он сделает свой сайт, например http://evil.com и заманит туда посетителя.

Когда посетитель зайдёт на http://evil.com, он автоматически запустит скрипт на странице. Этот скрипт сделает HTTP-запрос на почтовый сервер, к примеру, http://gmail.com. А ведь обычно HTTP-запросы идут с куками посетителя и другими авторизующими заголовками.

Поэтому хакер сможет написать на http://evil.com код, который, сделав GET-запрос на http://gmail.com, выдаст информацию из почтового ящика посетителя. Проанализировав её, сделаем ещё пачку POST-запросов для отправки писем от имени посетителя. Затем настанет очередь онлайн-банкинга и так далее.

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

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

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

  1. Простой метод: GET, POST или HEAD
  2. Простые заголовки – только из списка:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type со значением application/x-www-form-urlencoded, multipart/form-data или text/plain.

«Непростыми» считаются все остальные, например, запрос с методом PUT или с заголовком Authorization, который  не подходит под ограничения выше.

Принципиальная разница между ними заключается в том, что «простой» запрос можно сформировать и отправить на сервер и без XMLHttpRequest, например при помощи HTML-формы.

То есть, хакер на странице http://evil.com и до появления CORS мог отправить произвольный GET-запрос куда угодно. Например, если создать и добавить в документ элемент <script src=»любой url»>, то браузер  по  идее сделает GET-запрос на этот URL.

Читайте также  XMLHttpRequest метод POST

Аналогично, злой хакер и ранее мог на своей странице объявить и, при помощи JavaScript, отправить HTML-форму с методом GET/POST и кодировкой multipart/form-data.

А вот запросы с нестандартными заголовками или с методом DELETE таким образом создать не получится. Поэтому старый сервер может быть к ним не готов. Или, к примеру, он может полагать, что такие запросы веб-страница в принципе не умеет присылать, значит они пришли из привилегированного приложения, и дать им слишком много прав.

Поэтому при посылке «непростых» запросов нужно специальным образом спросить у сервера, согласен ли он в принципе на подобные кросс-доменные запросы или нет? И, если сервер не ответит, что согласен – значит, нет.

В спецификации CORS, как мы увидим далее, есть довольно  много деталей, но все они объединены единым принципом: новые возможности доступны только с явного согласия сервера (по умолчанию – отключены).

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

В кросс-доменный запрос браузер автоматически добавляет заголовок Origin, содержащий домен, с которого  и  был осуществлён запрос.

В случае запроса на http://anywhere.com/request с http://site.ru/page заголовки будут примерно такие:

GET /request
Host:anywhere.com
Origin:http://site.ru
...

Сервер должен, со своей стороны, ответить специальными заголовками, разрешает ли он такой запрос к себе.

Если сервер разрешает кросс-доменный запрос с этого домена – он должен добавить к ответу заголовок Access-Control-Allow-Origin, содержащий домен запроса (в данном случае «site.ru») или звёздочку *.

Только при наличии такого заголовка в ответе – браузер сочтёт запрос успешным, а иначе вернет ошибку.

То есть, ответ сервера может быть примерно таким:

HTTP/1.1 200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: http://site.ru

Если Access-Control-Allow-Origin нет, то браузер считает, что разрешение не получено, и завершает запрос с ошибкой.
При таких запросах не передаются куки и заголовки HTTP-авторизации. Параметры user и password в методе open игнорируются. Мы рассмотрим, как разрешить их передачу, чуть далее.

Что может сделать хакер, используя такие запросы?

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

Действительно, страница может сформировать любой GET/POST-запрос и отправить его, но без разрешения сервера ответа она не получит.

А без ответа такой запрос, по сути, эквивалентен отправке формы GET/POST, причём без авторизации.

Заголовки ответа

Чтобы JavaScript мог прочитать HTTP-заголовок ответа, сервер должен указать его имя в Access-Control-Expose-Headers.

Например:

HTTP/1.1 200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: http://site.ru
X-Uid: 123
X-Authorization: 2c9de507f2c54aa1
Access-Control-Expose-Headers: X-Uid, X-Authentication

По умолчанию скрипт может прочитать из ответа только «простые» заголовки:

Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma

То есть, Content-Type получить всегда можно, а доступ к специфическим заголовкам нужно открывать явно.

Читайте также  Метод fetch как замена XMLHttpRequest

Запросы от имени пользователя

По умолчанию браузер не передаёт с запросом куки и заголовки для авторизации.

Чтобы браузер передал вместе с запросом куки и авторизацию, нужно добавить запросу xhr.withCredentials = true:

var xhr = new XMLHttpRequest(); 
xhr.withCredentials = true; 
xhr.open('POST', 'http://anywhere.com/req', true) ...

Далее – всё как обычно, дополнительных действий со стороны клиента не требуется.

Такой XMLHttpRequest с куками, естественно, требует от сервера больше разрешений, чем «анонимный».

Поэтому для запросов с withCredentials предусмотрено дополнительное подтверждение со стороны сервера.

При запросе с withCredentials сервер должен вернуть уже не один, а 2 заголовка:

  • Access-Control-Allow-Origin: домен
  • Access-Control-Allow-Credentials: true

Пример заголовков:

HTTP/1.1 200 OK Content-Type:text/html; 
charset=UTF-8 
Access-Control-Allow-Origin: http://site.ru 
Access-Control-Allow-Credentials: true

Использование звёздочки * в Access-Control-Allow-Origin при этом запрещено.

Если этих заголовков не будет, то браузер не даст доступ к ответу сервера.

«Непростые» запросы

В кросс-доменном XMLHttpRequest можно указать не только GET/POST, но и любой другой метод, например PUT, DELETE.

Когда-то никто и не думал, что страница сможет сделать такие запросы. Поэтому ряд веб-сервисов написаны в предположении, что «если метод – нестандартный, то это не браузер». Некоторые веб-сервисы даже учитывают это при проверке прав доступа.

Чтобы пресечь любые недопонимания, браузер вынужден использовать предзапрос в случаях, когда:

  • Метод – не GET / POST / HEAD.
  • Заголовок Content-Type имеет значение отличное от application/x-www-form-urlencoded, multipart/form-data или text/plain, например application/xml.
  • Устанавливаются другие HTTP-заголовки, кроме Accept, Accept-Language, Content-Language.

Любое из условий выше ведёт к тому, что браузер сделает 2 HTTP-запроса.
Первый запрос называется «предзапрос» (английский термин «preflight»). Браузер делает его целиком по своей инициативе, из JavaScript мы о нём ничего не знаем, хотя можем увидеть это в инструментах разработчика.

Этот запрос использует метод OPTIONS. Он не содержит тела и содержит название желаемого метода в заголовке Access-Control-Request-Method, а если есть особые заголовки, то и их тоже – в Access-Control-Request-Headers.

Его задача – узнать у сервера, разрешает ли он использовать выбранный метод и заголовки.

На этот запрос сервер должен ответить статусом 200, без тела ответа, указав заголовки Access-Control-Allow-Method: метод и, при необходимости, Access-Control-Allow-Headers: разрешённые заголовки.

Дополнительно он может указать Access-Control-Max-Age: sec, где sec – количество секунд, на которые нужно закэшировать разрешение. Тогда при последующих вызовах метода браузер не будет делать предзапрос.
Давайте рассмотрим на конкретном примере.

Пример запроса COPY

Рассмотрим запрос COPY, который используется в протоколе WebDAV для управления файлами через HTTP:

var xhr = new XMLHttpRequest(); 
xhr.open('COPY', 'http://site.com/~vasya', true); 
xhr.setRequestHeader('Destination', 'http://site.com/~vasya.bak'); 
xhr.onload = ... 
xhr.onerror = ... 
xhr.send();

Этот запрос «непростой» по двум причинам (достаточно было бы одной из них):

  1. Метод COPY.
  2. Заголовок Destination.
Читайте также  COMET с XMLHttpRequest: непрерывные опросы

Поэтому браузер, по своей инициативе, шлёт предварительный запрос OPTIONS:

OPTIONS /~vasya HTTP/1.1
Host: site.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://site.ru
Access-Control-Request-Method: COPY
Access-Control-Request-Headers: Destination

Обратим внимание на детали:

  • Адрес – тот же, что и у основного запроса: http://site.com/~vasya.
  • Стандартные заголовки запроса Accept, Accept-Encoding, Connection присутствуют.
  • Кросс-доменные специальные заголовки запроса:
    • Origin – домен, с которого сделан запрос.
    • Access-Control-Request-Method – желаемый метод.
    • Access-Control-Request-Headers – желаемый «непростой» заголовок.

На этот запрос сервер должен ответить статусом 200, указав заголовки Access-Control-Allow-Method: COPY и Access-Control-Allow-Headers: Destination.
Но в протоколе WebDav разрешены многие методы и заголовки, которые имеет смысл сразу перечислить в ответе:

                     
HTTP/1.1 200 OK
Content-Type: text/plain
Access-Control-Allow-Methods: PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, 
UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST
Access-Control-Allow-Headers: Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, 
If-Modified-Since, X-File-Name, Cache-Control
Access-Control-Max-Age: 86400

Ответ должен быть без тела, то есть только заголовки.

Браузер видит, что метод COPY – в числе разрешённых и заголовок Destination – тоже, и дальше он шлёт уже основной запрос.

При этом ответ на предзапрос он закэширует на 86400 сек (сутки), так что последующие аналогичные вызовы сразу отправят основной запрос, без OPTIONS.

Основной запрос браузер выполняет уже в «обычном» кросс-доменном режиме:

COPY /~vasya HTTP/1.1
Host: site.com
Content-Type: text/html; charset=UTF-8
Destination: http://site.com/~vasya.bak
Origin: http://site.ru

Ответ сервера, согласно спецификации WebDav COPY, может быть таким:

HTTP/1.1 207 Multi-Status
Content-Type: text/xml; charset="utf-8"
Content-Length: ...
Access-Control-Allow-Origin: http://javascript.ru
<?xml version="1.0" encoding="utf-8" ?>
<d:multistatus xmlns:d="DAV:">
  ...
</d:multistatus>

Так как Access-Control-Allow-Origin содержит правильный домен, то браузер вызовет xhr.onload и запрос будет по сути завершён.

Итоги

  • Все браузеры умеют делать кросс-доменные XMLHttpRequest.
  • Кросс-доменный запрос всегда содержит заголовок Origin с доменом запроса.

Порядок выполнения:

  1. Для запросов с «непростым» методом или особыми заголовками браузер делает предзапрос OPTIONS, указывая их в Access-Control-Request-Method и Access-Control-Request-Headers. Браузер ожидает ответ со статусом 200, без тела, со списком разрешённых методов и заголовков в Access-Control-Allow-Method и Access-Control-Allow-Headers. Дополнительно можно указать Access-Control-Max-Age для того чтобы предзапрос кешировался.
  2. Браузер делает запрос и проверяет, есть ли в ответе Access-Control-Allow-Origin, равный * или Origin. Для запросов с withCredentials может быть только Origin и дополнительно Access-Control-Allow-Credentials: true.
  3. Если все проверки пройдены, то вызывается xhr.onload, иначе xhr.onerror, без деталей ответа.
  4. Дополнительно: названия нестандартных заголовков ответа сервер должен указать в Access-Control-Expose-Headers, если хочет, чтобы клиент мог их прочитать.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Поделиться

Об авторе

admin administrator

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: