2020年2月にリリースされるGoogle Chrome 80から、SameSite
属性がないCookieはSameSite=Lax
として扱われるようになります。
詳細は以下Google公式サイトを参照してください。
Google Developers Japan: 新しい Cookie 設定 SameSite=None; Secure の準備を始めましょう
今回は、DjangoでSameSite
属性に対応するにはどうすればいいのかについて解説したいと思います。
SameSite
属性についての詳しい解説については省略します。詳しくは以下の記事を参照してください。
SameSite cookies explained
Cookie の性質を利用した攻撃と Same Site Cookie の効果 | blog.jxck.io
DjangoでSameSite
に対応するには#
Djangoでは2.1からCookieにSameSite
属性を設定する機能が追加されました。
ただし、2.1は2019年12月2日をもってサポートを終了しています。公式サイトの「Supoorted Versions」を確認して、現時点でサポートされているバージョンを使うようにしましょう。
2.1からはHttpResponse.set_cookieにsamesite
が追加されました。
また、以下の設定が使えるようなっています。
3.0からは以下も追加されました。
サンプルアプリケーションを作って実際の挙動を検証#
では、実際にアプリケーションを作ってSameSite
属性の挙動を確認してみましょう。サンプルコードで使っている環境は以下のとおりです。
- Python 3.8.1
- Django 3.0.2
- django-extensions 2.2.5
- Werkzeug 0.16.0
- pyOpenSSL 19.1.0
- Google Chrome 79.0.3945.130
SameSite=None
を設定する場合はSecure属性が必須なので、django-extensions
・Werkzeug
・pyOpenSSL
でローカル環境でもhttps
でアクセスできるようにしています。
samesite_example
というプロジェクトを作成して、以下のようにコードを編集します。
samesite_example/settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 編集が必要な箇所のみ記載
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions', # これを追加
]
ALLOWED_HOSTS = [
'mysite.private', # これを追加
]
|
samesite_example/urls.py
1
2
3
4
5
6
7
8
9
10
| from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
path('set-cookies/', views.set_cookies),
path('get-cookies/', views.get_cookies),
path('admin/', admin.site.urls),
]
|
samesite_example/views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| from django.http import HttpResponse
from django.utils import timezone
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
def set_cookies(request):
formatted_now = timezone.now().isoformat()
response = HttpResponse('OK')
response.set_cookie('example-lax', formatted_now, secure=True, samesite='Lax')
response.set_cookie('example-strict', formatted_now, secure=True, samesite='Strict')
response.set_cookie('example-none', formatted_now, secure=True)
# Noneの場合だけ書き方を変える必要がある(理由は後述)
response.cookies['example-none']['samesite'] = 'None'
return response
@xframe_options_exempt
@csrf_exempt
def get_cookies(request):
keys = [
'example-lax',
'example-strict',
'example-none',
]
rows = []
for key in keys:
value = request.COOKIES.get(key, '')
rows.append(f'{key}={value}')
res = HttpResponse('<br>'.join(rows))
res['Access-Control-Allow-Origin'] = 'http://othersite.private:3000'
res['Access-Control-Allow-Credentials'] = 'true'
return res
|
Django 2.2・3.0では、HttpResponse.set_cookie
に渡せるsamesite
の値はLax
かStrict
のみです。None
は渡せません(渡すとValueError
が発生します)。
以下プルリクエストでNone
を渡せるよう対応してmaster
ブランチにはマージ済みですが、リリースは3.1を予定しているようです。
https://github.com/django/django/pull/11894
また、Djangoアプリケーションとは別に以下のindex.html
ファイルを用意します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Other Site</title>
</head>
<body>
<h1>Other Site</h1>
<h2>Case 1 (a tag)</h2>
<a target="_blank" href="https://mysite.private:8000/get-cookies/">https://mysite.private:8000/get-cookies/</a>
<h2>Case 2 (iframe tag)</h2>
<iframe src="https://mysite.private:8000/get-cookies/" frameborder="0"></iframe>
<h2>Case 3 (XMLHttpRequest GET Request)</h2>
<p id="case3-output">Result</p>
<script>
(function() {
function reqListener () {
let elem = document.getElementById('case3-output');
elem.innerHTML = this.responseText;
}
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener('load', reqListener);
xhr.open('GET', 'https://mysite.private:8000/get-cookies/');
xhr.send();
})();
</script>
<h2>Case 4 (XMLHttpRequest POST Request)</h2>
<p id="case4-output">Result</p>
<script>
(function() {
function reqListener () {
let elem = document.getElementById('case4-output');
elem.innerHTML = this.responseText;
}
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.addEventListener('load', reqListener);
xhr.open('POST', 'https://mysite.private:8000/get-cookies/');
xhr.send();
})();
</script>
<h2>Case 5 (GET Form)</h2>
<form action="https://mysite.private:8000/get-cookies/" method="GET">
<button>Send</button>
</form>
<h2>Case 6 (POST Form)</h2>
<form action="https://mysite.private:8000/get-cookies/" method="POST">
<button>Send</button>
</form>
</body>
</html>
|
/etc/hosts
(WindowsならC:\Windows\System32\drivers\etc\hosts
)に以下を書きます。
127.0.0.1 mysite.private othersite.private
以下コマンドでアプリケーションを起動します。
mysite.private
で表示させるサイト- Djangoプロジェクトの直下で
python manage.py runserver_plus --cert-file cert.crt
othersite.private
で表示させるサイトindex.html
があるディレクトリの直下でpython3.8 -m http.server -b 127.0.0.1 3000
https://mysite.private:8000/set-cookies/
をブラウザで開いてください。ブラウザ上にSameSite=Lax
・SameSite=Strict
・SameSite=None
を指定されたCookieが作られます。
次に、https://mysite.private:8000/get-cookies/
をブラウザで開いてください。上記URLで作られたCookieが送られ、それぞれが表示されているはずです。
今度はhttp://othersite.private:3000/
からクロスサイトでhttps://mysite.private:8000/get-cookies/
にリクエストを送ってみましょう。
http://othersite.private:3000/
では6種類のケースを用意して、それぞれ検証しています。
上記の結果を表にまとめます(「送信する」は太字にしました)。
SameSite | Case 1 (a tag) | Case 2 (iframe tag) | Case 3 (XMLHttpRequest GET Request) | Case 4 (XMLHttpRequest POST Request) | Case 5 (GET Form) | Case 6 (POST Form) |
---|
Lax | 送信する | 送信しない | 送信しない | 送信しない | 送信する | 送信しない |
Strict | 送信しない | 送信しない | 送信しない | 送信しない | 送信しない | 送信しない |
None | 送信する | 送信する | 送信する | 送信する | 送信する | 送信する |
None
はどのケースでもCookieを送信する(つまり従来どおり)、Strict
は逆にどのケースでも送信しない、Lax
はNone
とStrict
の中間のようなルールです。
ほとんどのウェブサービスではLax
が適切な値になるはずですが、ブログパーツのようにクロスサイトでのリクエストが必要な場合はNone
、セキュリティ要件が厳しい場合はStrict
を検討したほうがいいでしょう。