При столкновении файлов cookie через домены с помощью Samesite

Chrome

Введение

Для Четузаи,перекрестный доменЭто очень знакомый термин. Хотя браузеры беспокоятся о безопасности нашего веб-сайта, нам часто приходится обходить это ограничение, чтобы пользователи могли нормально обращаться к веб-сайту.corsЭто одно из часто используемых решений для решения междоменных задач.

установивAccess-Control-Allow-Credentials: trueа такжеxhr.withCredentials = true, который может реализовать междоменную передачуCookie, для достижения цели сохранения статуса входа пользователя. Эта схема хороша, но при неправильном использовании будетCSRFриски. Итак, изChrome 51запуск, браузерCookieдобавил новыйSameSiteимущество для предотвращенияCSRFАтака и отслеживание пользователей.

Характеристики тока отключены по умолчанию, нонекоторые пользователибыл затронут заранее. Если вы не можете использовать стороннюю функцию входа на некоторые веб-сайты, проверьте, не влияет ли на нее этот параметр.

испытай это сам

инвалидSameSiteпроверять

Хотя официальное заявление отChrome 79Пуск, эта функция будет включена по умолчанию (ранее утверждалось, что изChrome 80В начале была изменена последняя инструкция), но после тестирования выяснилось, что у некоторых пользователей эта функция по-прежнему отключена по умолчанию, поэтому сначала отключаем ееSameSiteПодтвердите и посмотрите, что произойдет.

ОткрытьChromeустановить, будетchrome://flags/#same-site-by-default-cookiesСначала отключите, а затем перезапустите браузер.

image

Используйте последний示例代码, имитируя операцию входа в систему локально для получения междоменногоCookie, а потом нестиCookieПолучить информацию о пользователе.

image

Можно обнаружить, что мы можем использовать запись на стороне сервера в обычном режиме.Cookieотправить запрос и получить информацию о пользователе, но будетConsoleувидеть предупреждающее сообщение.

image

Согласно последовательному принципу программиста «предупредил и проигнорировал», кажется, что мы можем игнорировать эту функцию. Но однаждыChromeБраузер полностью открытSameSiteфункция, и пользователь обновляет браузер, а затем на основеCookieВеб-сайты с междоменным входом не смогут войти в систему. Далее моделируем этот процесс.

включитьSameSiteпроверять

также открытьChromeустановить, будетchrome://flags/#same-site-by-default-cookiesВключите, затем перезапустите браузер.

пустойCookieи войдите снова. Уведомление:CookieОн находится под внутренним доменным именем, не очищайте соответствующее внешнее доменное имя.Cookie.

В это время мы можем найти: запрошенныйResponse CookiesВниз,SameSiteСвойство имеет подсказку, которая говорит намSameSiteСвойство не задано, будет использоваться значение по умолчаниюLax.

image

В настоящее время, если вы попытаетесь получить информацию о пользователе, вы не сможете получить ее успешно, потому чтоCookieНе доводится до серверной службы вместе с запросом. При осмотре было установлено, чтоCookieНе удалось записать в браузер пользователя.

image

Видно, что если вы не хотите потерпеть неудачу в 2020 году, то начинать заниматься этой проблемой нужно заранее.

иметь дело сSameSiteпроверять

SameSiteзначение свойства по умолчаниюLaxТолько разрешеноgetзапросить переносCookie, что явно не выполняется, поэтому будемSameSiteЗначение свойства меняется наNone, в то время какsecureсвойство установлено наtrue. Это также означает, что доменное имя вашей серверной службы должно использоватьсяhttpsДоступ к протоколу.

// 注意:cookie 模块必需要更新到最新的版本(0.4.0),才支持 sameSite=none
res.cookie('token', 'token 123', { maxAge: 2592000000, httpOnly: true, sameSite: 'none', secure: true, });

Попробуйте еще раз, чтобы убедиться, что проблема решена.

image

Однако это лишь временное решение, так как установкаsameSiteдляNoneПозже,CSRFРиск вернулся. Итак, замените его наtokenспособ проверки, не полагаясь наCookie, может быть более разумным решением.

Полный пример

Ниже приведен полный пример кода иNginxконфигурация.

app.js

var path = require('path');
var cors = require('cors');
var express = require('express');
var cookieParser = require('cookie-parser');

var app = express();

app.use(cors({ origin: true, credentials: true, }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); // index.html放在public目录下

app.get('/api/info', function login(req, res) {
    let token = req.cookies['token'];

    if (token) {
        res.json({ success: true, data: { name: 'somebody', age: 21 } });
    } else {
        res.json({ success: false, message: '请先登录' });
    }
});

app.get('/api/login', function login(req, res) {
    res.cookie('token', 'token 123', { maxAge: 2592000000, httpOnly: true, });
    res.end();
});

app.get('/api/login/security', function login(req, res) {
    // 注意:cookie 模块必需要更新到最新的版本(0.4.0),才支持 sameSite=none
    res.cookie('token', 'token 123', { maxAge: 2592000000, httpOnly: true, sameSite: 'none', secure: true, });
    res.end();
});

app.listen(8888, function () {
    console.log('http://localhost:8888');
});

index.html

<html>

<head>
  <title>Demo</title>
  <style>
    button {
      width: 80px;
      height: 32px;
      line-height: 32px;
      text-align: center;
    }
  </style>
</head>

<body>
<div>
  <p>
    <button id="login">登录</button>
    <button id="security">安全登录</button>
  </p>
  <p>
    <button id="check">查询</button>
  </p>
</div>

<script>
  (function() {
    var get = function get(url, callback) {
      var xhr = new XMLHttpRequest();
      xhr.withCredentials = true;
      xhr.open('get', url);
      xhr.onreadystatechange = function onreadystatechange() {
        if (xhr.readyState === 4) {
          var res = xhr.response;

          try {
            res = JSON.parse(res);
          } catch (e) {}

          typeof callback === 'function' && callback(res);
        }
      };
      xhr.send(null);
    };

    var login = document.querySelector('#login');
    var check = document.querySelector('#check');
    var security = document.querySelector('#security');

    login.addEventListener('click', function onLogin() {
      get('https://api.server.cn/login');
    });

    check.addEventListener('click', function onLogin() {
      get('https://api.server.cn/info', function callback(res) {
        if (res.success) {
          console.log(res.data);
        } else {
          alert(res.message);
        }
      });
    });

    security.addEventListener('click', function onLogin() {
      get('https://api.server.cn/login/security');
    });
  })();
</script>
</body>

</html>

nginx.conf

server {
    listen               443 ssl;
    server_name          api.server.cn;
    ssl_certificate      /path/to/ssl/server.crt;
    ssl_certificate_key  /path/to/ssl/server.key;
    ssl_ciphers          HIGH:!aNULL:!MD5;

    location / {
        proxy_pass  http://localhost:8888/api/;
    }
}

hosts

127.0.0.1 api.server.cn

Уведомление

Поскольку наш сертификат является самоподписанным и не может пройти проверку сертификата браузера, нам нужно вручную нажать «Продолжить до xxx (небезопасно)», чтобы нормально отправлять запросы к серверной службе.

image

Ссылаться на