Что такое CORS

Spread the love
  • 5
    Поделились

Перевод статьи от том что такое Same-origin policy и CORS и зачем они нужны. Оригинальная статьи Martin SplittUnderstanding CORS

TL;DR

  • Браузер использует Same-origin policy, чтобы не обрабатывать AJAX ответы от веб-сайтов расположенных на адресах отличных от адреса с которого была загружена веб страница.
  • Same-origin policy не запрещает генерировать запросы к другим сайтам, но запрещает обрабатывать от них ответ.
  • CORS (Cross-Origin Resource Sharing) механизм, который использует дополнительные заголовки HTTP, чтобы дать браузерам указание предоставить веб-приложению, работающему в одном источнике, доступ к ответу на запрос к ресурсам из другого источника.
  • CORS вместе с credentials (с данными аутентификации) требует осторожности.
  • CORS это браузерная политика. Другие приложения не затрагиваются этим понятием.

Давайте начнем с примера.

Пример

В этом примере я рассмотрю только код обработки запроса, полный пример доступен на Github.

Скажем, у нас есть замечательный веб-сайт на Node.js с открытым API, доступный по адресу http://good.com:8000/public. Пусть там будет примерно такая функция получения GET запроса:

app.get('/public', function(req, res) {
  res.send(JSON.stringify({
    message: 'This is public'
  }));
})

У нас также есть простая функция входа в систему, где пользователи вводят общее секретное слово secret и им затем им устанавливается cookie, идентифицируя их как аутентифицированных:

app.post('/login', function(req, res) {
  if(req.body.password === 'secret') {
    req.session.loggedIn = true
    res.send('You are now logged in!')
  } else {
    res.send('Wrong password.')
  }
})

И пусть у нас будет некое приватное API для каких нибудь личных данных в /private, только для аутентифицированных пользователей.

app.get('/private', function(req, res) {
  if(req.session.loggedIn === true) {
    res.send(JSON.stringify({
      message: 'THIS IS PRIVATE'
    }))
  } else {
    res.send(JSON.stringify({
      message: 'Please login first'
    }))
  }
})

Запрос нашего API через AJAX из других доменов

И допустим у нас есть какое-нибудь клиентское приложение работающее с нашим API. Но предположим что, наше API находится по адресу http://good.com:300/public, а наш клиент размещен на http://thirdparty.com, и на клиенте есть следующий код:

fetch('http://good.com:3000/public')
  .then(response => response.text())
  .then((result) => {
    document.body.textContent = result
  })

И это не будет работать!

Если мы посмотрим на вкладку network в консоле Хрома при обращение от http://thirdparty.com к http://good.com:

The network request was successful
источник: https://res.cloudinary.com/practicaldev/image/fetch/s–2T2IvlfO–/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/3mbnano5018lyt3g1lh7.png

Сам по себе запрос был успешным, но результат оказался не доступен. Описание причины можно найти в консоли JavaScript:

The console shows that a missing CORS header causes the problem
источник: https://res.cloudinary.com/practicaldev/image/fetch/s–zxV3mrFa–/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/oi16nh5dvb6mws9fsmil.png

Ага! Нам не хватает заголовка Access-Control-Allow-Origin. Но зачем он нам и для чего он вообще нужен?

Same-Origin Policy

Причиной, по которой мы не получим ответ в JavaScript, является Same-Origin Policy. Эта ограничительная мера была придумана разработчиками браузеров что бы веб-сайт не мог получить ответ на сгенерированный AJAX запрос к другому веб-сайту находящемуся по другому адресу .

Например: если вы заходите на example.org, вы бы не хотели, чтобы этот веб-сайт отправлял запрос к примеру на ваш банковский веб-сайт и получал баланс вашего счета и транзакции.

Same-Origin Policy предотвращает именно это.

«источник (origin)» в этом случае состоит из

  • протокол (например http)
  • хост (например example.com)
  • порт (например 8000)

Так что http://example.org и http://www.example.org и https://example.org – это три разных источника.

Пару слов о CSRF

Обратите внимание, что существует класс атак, называемый подделкой межсайтовых запросов (Cross Site Request Forgerycsrf ), от которых не защищает Same-Origin Policy.

При CSRF-атаке злоумышленник отправляет запрос сторонней странице в фоновом режиме, например, отправляя POST запрос на веб-сайт вашего банка. Если у вас в этот момент есть действительный сеанс с вашим банком, любой веб-сайт может сгенерировать запрос в фоновом режиме, который будет выполнен, если ваш банк не использует контрмеры против CSRF.

Так же обратите внимание, что, несмотря на то, что действует Same-Origin Policy, наш пример запроса с сайта secondparty.com на сайте good.com будет успешно выполнен – мы просто не соможем получить доступ к результатам. Но для CSRF нам не нужен результат …

Например, API, которое позволяет отправлять электронные письма, выполняя POST запрос, отправит электронное письмо, если мы предоставим ему правильные данные. Злоумышленнику не нужно заботится о результате, его забота это отправляемое электронное письмо, которое он получит независимо от возможности видеть ответ от API.

Включение CORS для нашего публичного API

Допустим нам нужно разрешить работу JavaScript на сторонних сайтах (например, thirdparty.com) что бы получать доступ к нашим ответам API. Для этого нам нужно включить CORS в заголовок ответа от сервера. Это делается на стороне сервера:

app.get('/public', function(req, res) {
  res.set('Access-Control-Allow-Origin', '*')
  res.send(...)
})

Здесь мы устанавливаем заголовку Access-Control-Allow-Origin значение *, что означает: что любому хосту разрешен доступ к этому URL и ответу в браузере:

The response is available once we set the CORS header
источник: https://res.cloudinary.com/practicaldev/image/fetch/s–PJfV7RvQ–/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/http://50linesofco.de/images/post-images/cors/basic-cors.png

Непростые запросы и предварительные запросы (preflights)

Предыдущий пример был так называемым простым запросом. Простые запросы – это запросы GET или POST с несколькими разрешенными заголовками и значениями заголовков.

Допустим теперь Thirdparty.com немного меняет реализацию, и теперь он обрабатывает запросы в формате JSON:

fetch('http://good.com:3000/public', {
  headers: {
    'Content-Type': 'application/json'
  }
})
  .then(response => response.json())
  .then((result) => {
    document.body.textContent = result.message
  })

Но это снова ломает Thirdparty.com!
На этот раз консоль показывает другую ошибку:

The request has been preflighted with an OPTIONS request
источник: https://res.cloudinary.com/practicaldev/image/fetch/s–C4Eg4xnt–/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/http://50linesofco.de/images/post-images/cors/cors-preflight.png

Не простые запросы – это любой запрос, который использует метод, который не является GET или POST или использует тип контента, который не:

  • text/plain
  • application/x-www-form-urlencoded
  • multipart/form-data

То есть любой другой заголовок, который не разрешен для простых запросов, требует предварительного запроса (preflight request).

Этот механизм позволяет веб-серверам решать, хотят ли они разрешить фактический запрос. Браузер устанавливает заголовки Access-Control-Request-Headers и Access-Control-Request-Method, чтобы сообщить серверу, какой запрос ожидать, и сервер должен ответить соответствующими заголовками.

Но наш сервер еще не отвечает с этими заголовками, поэтому нам нужно добавить их:

app.get('/public', function(req, res) {
  res.set('Access-Control-Allow-Origin', '*')
  res.set('Access-Control-Allow-Methods', 'GET, OPTIONS')
  res.set('Access-Control-Allow-Headers', 'Content-Type')
  res.send(JSON.stringify({
    message: 'This is public info'
  }))
})

Теперь Thirdparty.com снова может получить доступ к ответу.

Credentials и CORS

Теперь давайте предположим, что нам нужно залогинится на good.com что бы получить доступ к /private с конфиденциальной информацией.

При всех наших настройках CORS может ли другой сайт, скажем evil.com, так же получить эту конфиденциальную информацию?

Давайте посмотрим:

fetch('http://good.com:3000/private')
  .then(response => response.text())
  .then((result) => {
    let output = document.createElement('div')
    output.textContent = result
    document.body.appendChild(output)
  })

Независимо от того, попытаемся ли мы залогинится на good.com или нет, мы увидим «Please login first».

Причина в том, что cookie от good.com не будут отправляться, когда запрос поступает из другого источника. Мы можем попросить браузер отправить файлы cookie клиенту, даже если запрос с других доменов:

fetch('http://good.com:3000/private', {
  credentials: 'include'
})
  .then(response => response.text())
  .then((result) => {
    let output = document.createElement('div')
    output.textContent = result
    document.body.appendChild(output)
  })

Но опять же это не будет работать в браузере. И это хорошая новость, на самом деле.

Итак, мы не хотим, чтобы evil.com имел доступ к приватным данным, но что, если мы хотим, чтобы thirdparty.com имел доступ к /private?
В этом случае нам нужно установить для заголовка Access-Control-Allow-Credentials значение true:

app.get('/private', function(req, res) {
  res.set('Access-Control-Allow-Origin', '*')
  res.set('Access-Control-Allow-Credentials', 'true')
  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

Но это все равно пока еще не сработает. Это опасная практика – разрешать любые аутентифицированные запросы с других источников.

Браузер не позволит нам так легко совершить ошибку.

Если мы хотим разрешить Thirdparty.com доступ к /private, нам нужно указать точный источник в заголовке:

app.get('/private', function(req, res) {
  res.set('Access-Control-Allow-Origin', 'http://thirdparty.com:8000')
  res.set('Access-Control-Allow-Credentials', 'true')
  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

Теперь http://thirdparty:8000 также имеет доступ к приватным данным, в то время как запрос с evil.com будет заблокирован.

Разрешить множественные источники (origin)

Теперь мы разрешили одному источнику делать запросы к другому источнику с данными аутентификации. Но что, если у нас есть несколько других источников?

В этом случае мы, вероятно, хотим использовать белый список:

const ALLOWED_ORIGINS = [
  'http://anotherthirdparty.com:8000',
  'http://thirdparty.com:8000'
]

app.get('/private', function(req, res) {
  if(ALLOWED_ORIGINS.indexOf(req.headers.origin) > -1) {
    res.set('Access-Control-Allow-Credentials', 'true')
    res.set('Access-Control-Allow-Origin', req.headers.origin)
  } else { // разрешить другим источникам отправлять неподтвержденные запросы CORS
    res.set('Access-Control-Allow-Origin', '*')        
  }

  // let caches know that the response depends on the origin
  res.set('Vary', 'Origin');

  if(req.session.loggedIn === true) {
    res.send('THIS IS THE SECRET')
  } else {
    res.send('Please login first')
  }
})

Опять же: не отправляйте напрямую req.headers.origin в качестве разрешенного заголовка CORS. Это позволит любому веб-сайту получить доступ к приватным данным.
Из этого правила могут быть исключения, но, по крайней мере, дважды подумайте, прежде чем внедрять CORS с учетными данными без белого списка.

Заключение

В этой статье мы рассмотрели Same-Origin Policy и то, как мы можем использовать CORS, чтобы разрешать запросы между источниками, когда это необходимо.

Это требует настройки на стороне сервера и на стороне клиента и в зависимости от запроса вызовет предварительный (preflight) запрос.

При работе с аутентифицированными запросами перекрестного происхождения следует проявлять дополнительную осторожность. Белый список может помочь разрешить нескольким источникам без риска утечки конфиденциальных данных (которые защищены аутентификацией).

Была ли вам полезна эта статья?
[8 / 5]

Spread the love
  • 5
    Поделились

Добавить комментарий

Ваш e-mail не будет опубликован.