Skip to content

Commit c736949

Browse files
authored
Merge pull request isso-comments#700 from ix5/secure-cookie
Improve cookie SameSite/secure handling
2 parents 6a5418c + 2919461 commit c736949

File tree

6 files changed

+132
-14
lines changed

6 files changed

+132
-14
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ Changelog for Isso
88

99
- Don't ignore missing configuration files.
1010
(Jelmer Vernooij)
11+
- New "samesite" option in [server] section to override SameSite header for
12+
cookies. (#700, ix5)
13+
- Fallback for SameSite header depending on whether host is served over https
14+
or http (#700, ix5)
1115

1216
0.12.4 (2021-02-03)
1317
-------------------

docs/docs/configuration/server.rst

+11
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,17 @@ trusted-proxies
202202
`X-Forwarded-For` HTTP header, which is important for the mechanism
203203
forbiding several comment votes coming from the same subnet.
204204

205+
samesite
206+
override ``Set-Cookie`` header ``SameSite`` value.
207+
Needed for setups where isso is not hosted on the same domain, e.g. called
208+
from example.org and hosted under comments.example.org.
209+
By default, isso will set ``SameSite=None`` when served over https and
210+
``SameSite=Lax`` when served over http
211+
(see `MDM: SameSite <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite>`_
212+
and `#682 <https://github.com/posativ/isso/issues/682>`_ for details).
213+
214+
Accepted values: ``None``, ``Lax``, ``Strict``
215+
205216
.. _configure-smtp:
206217

207218
SMTP

isso/tests/fixtures.py

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ def __call__(self, environ, start_response):
1616
return self.app(environ, start_response)
1717

1818

19+
class FakeHost(object):
20+
21+
def __init__(self, app, host, scheme):
22+
self.app = app
23+
self.host = host
24+
self.scheme = scheme
25+
26+
def __call__(self, environ, start_response):
27+
environ['HTTP_HOST'] = self.host
28+
environ['wsgi.url_scheme'] = self.scheme
29+
return self.app(environ, start_response)
30+
31+
1932
class JSONClient(Client):
2033

2134
def open(self, *args, **kwargs):

isso/tests/test_comments.py

+65-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from isso.utils import http
1818
from isso.views import comments
1919

20-
from fixtures import curl, loads, FakeIP, JSONClient
20+
from fixtures import curl, loads, FakeIP, FakeHost, JSONClient
2121
http.curl = curl
2222

2323

@@ -495,6 +495,70 @@ def testLatestNotEnabled(self):
495495
self.assertEqual(response.status_code, 404)
496496

497497

498+
class TestHostDependent(unittest.TestCase):
499+
500+
def setUp(self):
501+
fd, self.path = tempfile.mkstemp()
502+
conf = config.load(os.path.join(dist.location, dist.project_name, "defaults.ini"))
503+
conf.set("general", "dbpath", self.path)
504+
self.conf = conf
505+
506+
class App(Isso, core.Mixin):
507+
pass
508+
509+
self.app = App(conf)
510+
511+
self.client = JSONClient(self.app, Response)
512+
self.post = self.client.post
513+
514+
def tearDown(self):
515+
os.unlink(self.path)
516+
517+
def testSecureCookieNoConf(self):
518+
self.app.wsgi_app = FakeHost(self.app.wsgi_app, "isso-dev.local", "https")
519+
rv = self.post('/new?uri=%2Fpath%2F',
520+
data=json.dumps({'text': 'Lorem ipsum ...'}))
521+
522+
self.assertIn("Secure", rv.headers["Set-Cookie"])
523+
self.assertIn("Secure", rv.headers["X-Set-Cookie"])
524+
self.assertIn("SameSite=None", rv.headers["Set-Cookie"])
525+
526+
def testInSecureCookieNoConf(self):
527+
self.app.wsgi_app = FakeHost(self.app.wsgi_app, "isso-dev.local", "http")
528+
rv = self.post('/new?uri=%2Fpath%2F',
529+
data=json.dumps({'text': 'Lorem ipsum ...'}))
530+
531+
self.assertNotIn("Secure", rv.headers["Set-Cookie"])
532+
self.assertNotIn("Secure", rv.headers["X-Set-Cookie"])
533+
self.assertIn("SameSite=Lax", rv.headers["Set-Cookie"])
534+
535+
def testSameSiteConfNone(self):
536+
# By default, isso should set SameSite=Lax when served over http
537+
self.app.wsgi_app = FakeHost(self.app.wsgi_app, "isso-dev.local", "http")
538+
# Conf overrides SameSite setting
539+
self.conf.set("server", "samesite", "None")
540+
541+
rv = self.post('/new?uri=%2Fpath%2F',
542+
data=json.dumps({'text': 'Lorem ipsum ...'}))
543+
544+
self.assertNotIn("Secure", rv.headers["Set-Cookie"])
545+
self.assertNotIn("Secure", rv.headers["X-Set-Cookie"])
546+
self.assertIn("SameSite=None", rv.headers["Set-Cookie"])
547+
548+
def testSameSiteConfLax(self):
549+
# By default, isso should set SameSite=None when served over https
550+
self.app.wsgi_app = FakeHost(self.app.wsgi_app, "isso-dev.local", "https")
551+
# Conf overrides SameSite setting
552+
self.conf.set("server", "samesite", "Lax")
553+
554+
rv = self.post('/new?uri=%2Fpath%2F',
555+
data=json.dumps({'text': 'Lorem ipsum ...'}))
556+
557+
self.assertIn("Secure", rv.headers["Set-Cookie"])
558+
self.assertIn("Secure", rv.headers["X-Set-Cookie"])
559+
self.assertIn("SameSite=Lax", rv.headers["Set-Cookie"])
560+
561+
498562
class TestModeratedComments(unittest.TestCase):
499563

500564
def setUp(self):

isso/views/comments.py

+29-13
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,9 @@ def new(self, environ, request, uri):
309309
# notify extension, that the new comment has been successfully saved
310310
self.signal("comments.new:after-save", thread, rv)
311311

312-
cookie = functools.partial(dump_cookie,
313-
value=self.isso.sign(
314-
[rv["id"], sha1(rv["text"])]),
315-
max_age=self.conf.getint('max-age'))
312+
cookie = self.create_cookie(
313+
value=self.isso.sign([rv["id"], sha1(rv["text"])]),
314+
max_age=self.conf.getint('max-age'))
316315

317316
rv["text"] = self.isso.render(rv["text"])
318317
rv["hash"] = self.hash(rv['email'] or rv['remote_addr'])
@@ -348,6 +347,24 @@ def _remote_addr(self, request):
348347
if addr not in self.trusted_proxies), remote_addr)
349348
return utils.anonymize(str(remote_addr))
350349

350+
def create_cookie(self, **kwargs):
351+
"""
352+
Setting cookies to SameSite=None requires "Secure" attribute.
353+
For http-only, we need to override the dump_cookie() default SameSite=None
354+
or the cookie will be rejected.
355+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure
356+
"""
357+
isso_host_script = self.isso.conf.get("server", "public-endpoint") or local.host
358+
samesite = self.isso.conf.get("server", "samesite")
359+
if isso_host_script.startswith("https://"):
360+
secure = True
361+
samesite = samesite or "None"
362+
else:
363+
secure = False
364+
samesite = samesite or "Lax"
365+
return functools.partial(dump_cookie, **kwargs,
366+
secure=secure, samesite=samesite)
367+
351368
"""
352369
@api {get} /id/:id view
353370
@apiGroup Comment
@@ -458,10 +475,9 @@ def edit(self, environ, request, id):
458475

459476
self.signal("comments.edit", rv)
460477

461-
cookie = functools.partial(dump_cookie,
462-
value=self.isso.sign(
463-
[rv["id"], sha1(rv["text"])]),
464-
max_age=self.conf.getint('max-age'))
478+
cookie = self.create_cookie(
479+
value=self.isso.sign([rv["id"], sha1(rv["text"])]),
480+
max_age=self.conf.getint('max-age'))
465481

466482
rv["text"] = self.isso.render(rv["text"])
467483

@@ -474,7 +490,7 @@ def edit(self, environ, request, id):
474490
@api {delete} '/id/:id' delete
475491
@apiGroup Comment
476492
@apiDescription
477-
Delte an existing comment. Deleting a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details.
493+
Delete an existing comment. Deleting a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details.
478494
479495
@apiParam {number} id
480496
Id of the comment to delete.
@@ -518,7 +534,8 @@ def delete(self, environ, request, id, key=None):
518534
self.signal("comments.delete", id)
519535

520536
resp = JSON(rv, 200)
521-
cookie = functools.partial(dump_cookie, expires=0, max_age=0)
537+
cookie = self.create_cookie(expires=0, max_age=0)
538+
522539
resp.headers.add("Set-Cookie", cookie(str(id)))
523540
resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id))
524541
return resp
@@ -1107,9 +1124,8 @@ def login(self, env, req):
11071124
'/admin',
11081125
get_current_url(env, strip_querystring=True)
11091126
))
1110-
cookie = functools.partial(dump_cookie,
1111-
value=self.isso.sign({"logged": True}),
1112-
expires=datetime.now() + timedelta(1))
1127+
cookie = self.create_cookie(value=self.isso.sign({"logged": True}),
1128+
expires=datetime.now() + timedelta(1))
11131129
response.headers.add("Set-Cookie", cookie("admin-session"))
11141130
response.headers.add("X-Set-Cookie", cookie("isso-admin-session"))
11151131
return response

share/isso.conf

+10
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ profile = off
119119
# forbiding several comment votes coming from the same subnet.
120120
trusted-proxies =
121121

122+
# Override Set-Cookie header SameSite value.
123+
# Needed for setups where isso is not hosted on the same domain, e.g. called
124+
# from example.org and hosted under comments.example.org.
125+
# By default, isso will set SameSite=None when served over https and
126+
# SameSite=Lax when served over http.
127+
# See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
128+
# and https://github.com/posativ/isso/issues/682
129+
# Accepted values: None, Lax, Strict
130+
samesite =
131+
122132

123133
[smtp]
124134
# Isso can notify you on new comments via SMTP. In the email notification, you

0 commit comments

Comments
 (0)