Skip to content

Commit b622fdf

Browse files
committed
Cookies: set samesite: Lax by default
The django's default is `Lax`, so I'm removing the setting. I'm overriding only for .org in our ops repos. We also are only allowing cross site requests from public versions only. Even if the domain is known, if the version is private we don't set the CORS header.
1 parent 0c6ae52 commit b622fdf

File tree

4 files changed

+23
-37
lines changed

4 files changed

+23
-37
lines changed

readthedocs/core/middleware.py

+6-12
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,16 @@
22
import time
33

44
from django.conf import settings
5-
from django.contrib.sessions.backends.base import SessionBase
6-
from django.contrib.sessions.backends.base import UpdateError
5+
from django.contrib.sessions.backends.base import SessionBase, UpdateError
76
from django.contrib.sessions.middleware import SessionMiddleware
8-
from django.core.exceptions import SuspiciousOperation
9-
from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
10-
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
11-
from django.http import Http404, HttpResponseBadRequest
12-
from django.urls.base import set_urlconf
7+
from django.core.exceptions import (
8+
ImproperlyConfigured,
9+
MiddlewareNotUsed,
10+
SuspiciousOperation,
11+
)
1312
from django.utils.cache import patch_vary_headers
14-
from django.utils.deprecation import MiddlewareMixin
1513
from django.utils.http import http_date
1614
from django.utils.translation import ugettext_lazy as _
17-
from django.shortcuts import render
18-
19-
from readthedocs.projects.models import Domain, Project
20-
2115

2216
log = logging.getLogger(__name__)
2317

readthedocs/core/signals.py

+3-21
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
"""Signal handling for core app."""
22

33
import logging
4-
from urllib.parse import urlparse
54

65
from corsheaders import signals
76
from django.conf import settings
8-
from django.db.models import Count, Q
7+
from django.db.models import Count
98
from django.db.models.signals import pre_delete
109
from django.dispatch import Signal, receiver
1110
from rest_framework.permissions import SAFE_METHODS
1211

1312
from readthedocs.builds.models import Version
1413
from readthedocs.core.unresolver import unresolve
15-
from readthedocs.projects.models import Domain, Project
14+
from readthedocs.projects.models import Project
1615

1716
log = logging.getLogger(__name__)
1817

@@ -49,8 +48,7 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen
4948
* It's a safe HTTP method
5049
* The origin is in ALLOWED_URLS
5150
* The URL is owned by the project that they are requesting data from
52-
* The version is public or the domain is linked to the project
53-
(except for the embed API).
51+
* The version is public
5452
5553
.. note::
5654
@@ -62,8 +60,6 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen
6260
if 'HTTP_ORIGIN' not in request.META or request.method not in SAFE_METHODS:
6361
return False
6462

65-
host = urlparse(request.META['HTTP_ORIGIN']).netloc.split(':')[0]
66-
6763
# Always allow the sustainability API,
6864
# it's used only on .org to check for ad-free users.
6965
if _has_donate_app() and request.path_info.startswith('/api/v2/sustainability'):
@@ -103,20 +99,6 @@ def decide_if_cors(sender, request, **kwargs): # pylint: disable=unused-argumen
10399
if is_public:
104100
return True
105101

106-
# Don't check for known domains for the embed api.
107-
# It gives a lot of information,
108-
# we should use a list of trusted domains from the user.
109-
if valid_url == '/api/v2/embed':
110-
return False
111-
112-
# Or allow if they have a registered domain
113-
# linked to that project.
114-
domain = Domain.objects.filter(
115-
Q(domain__iexact=host),
116-
Q(project=project) | Q(project__subprojects__child=project),
117-
)
118-
if domain.exists():
119-
return True
120102
return False
121103

122104

readthedocs/rtd_tests/tests/test_middleware.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_allow_linked_domain_from_public_version(self):
7272
resp = self.middleware.process_response(request, {})
7373
self.assertIn('Access-Control-Allow-Origin', resp)
7474

75-
def test_allow_linked_domain_from_private_version(self):
75+
def test_dont_allow_linked_domain_from_private_version(self):
7676
self.version.privacy_level = PRIVATE
7777
self.version.save()
7878
request = self.factory.get(
@@ -81,7 +81,7 @@ def test_allow_linked_domain_from_private_version(self):
8181
HTTP_ORIGIN='http://my.valid.domain',
8282
)
8383
resp = self.middleware.process_response(request, {})
84-
self.assertIn('Access-Control-Allow-Origin', resp)
84+
self.assertNotIn('Access-Control-Allow-Origin', resp)
8585

8686
def test_allowed_api_public_version_from_another_domain(self):
8787
request = self.factory.get(
@@ -228,6 +228,7 @@ def setUp(self):
228228

229229
self.user = create_user(username='owner', password='test')
230230

231+
@override_settings(SESSION_COOKIE_SAMESITE=None)
231232
def test_fallback_cookie(self):
232233
request = self.factory.get('/')
233234
response = HttpResponse()
@@ -238,6 +239,7 @@ def test_fallback_cookie(self):
238239
self.assertTrue(settings.SESSION_COOKIE_NAME in response.cookies)
239240
self.assertTrue(self.middleware.cookie_name_fallback in response.cookies)
240241

242+
@override_settings(SESSION_COOKIE_SAMESITE=None)
241243
def test_main_cookie_samesite_none(self):
242244
request = self.factory.get('/')
243245
response = HttpResponse()
@@ -247,3 +249,13 @@ def test_main_cookie_samesite_none(self):
247249

248250
self.assertEqual(response.cookies[settings.SESSION_COOKIE_NAME]['samesite'], 'None')
249251
self.assertEqual(response.cookies[self.middleware.cookie_name_fallback]['samesite'], '')
252+
253+
def test_main_cookie_samesite_lax(self):
254+
request = self.factory.get('/')
255+
response = HttpResponse()
256+
self.middleware.process_request(request)
257+
request.session['test'] = 'value'
258+
response = self.middleware.process_response(request, response)
259+
260+
self.assertEqual(response.cookies[settings.SESSION_COOKIE_NAME]['samesite'], 'Lax')
261+
self.assertTrue(self.test_main_cookie_samesite_none not in response.cookies)

readthedocs/settings/base.py

-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ class CommunityBaseSettings(Settings):
7373
SESSION_COOKIE_HTTPONLY = True
7474
SESSION_COOKIE_AGE = 30 * 24 * 60 * 60 # 30 days
7575
SESSION_SAVE_EVERY_REQUEST = True
76-
# This cookie is used in cross-origin API requests from *.readthedocs.io to readthedocs.org
77-
SESSION_COOKIE_SAMESITE = None
7876

7977
# CSRF
8078
CSRF_COOKIE_HTTPONLY = True

0 commit comments

Comments
 (0)