Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion kobo/apps/external_integrations/migrations/0002_do_nothing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='corsmodel',
name='cors',
field=models.CharField(help_text='Must contain exactly the URI scheme, host, and port, e.g. https://example.com:1234. Standard ports (80 for http and 443 for https) may be omitted.', max_length=255, verbose_name='allowed origin'),
field=models.CharField(
help_text=(
'Must contain exactly the URI scheme and host, e.g.'
' https://example.com. Include a port only if it is'
' non-standard, e.g. https://example.com:1234. Do not'
' include the default port for the scheme (80 for http, 443'
' for https): browsers never send it in the `Origin`'
' header, so it would never match.'
),
max_length=255,
verbose_name='allowed origin',
),
),
]
26 changes: 22 additions & 4 deletions kobo/apps/external_integrations/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# coding: utf-8
from django.core.exceptions import ValidationError
from django.db import models


Expand All @@ -11,12 +12,29 @@ class CorsModel(models.Model):
cors = models.CharField(
max_length=255,
verbose_name='allowed origin',
help_text=
'Must contain exactly the URI scheme, host, and port, e.g. '
'https://example.com:1234. Standard ports (80 for http and 443 '
'for https) may be omitted.'
help_text=(
'Must contain exactly the URI scheme and host, e.g. '
'https://example.com. Include a port only if it is non-standard, '
'e.g. https://example.com:1234. Do not include the default port '
'for the scheme (80 for http, 443 for https): browsers never send '
'it in the `Origin` header, so it would never match.'
),
)

def clean(self):
super().clean()
for scheme, port in ('http:', '80'), ('https:', '443'):
if self.cors.startswith(scheme) and self.cors.endswith(f':{port}'):
raise ValidationError(
{
'cors': (
f'Do not include the default port ({port}) for '
f'{scheme[:-1]}. Browsers never send it in the '
'`Origin` header, so this entry would never match.'
)
}
)

def __str__(self):
return self.cors

Expand Down
4 changes: 3 additions & 1 deletion kobo/apps/external_integrations/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

def cors_allow_external_sites(sender, request, **kwargs):
origin = request.headers.get('origin')
return CorsModel.objects.filter(cors=origin).exists()
if not origin:
return False
return CorsModel.objects.filter(cors__iexact=origin).exists()


check_request_enabled.connect(cors_allow_external_sites)
19 changes: 19 additions & 0 deletions kobo/apps/external_integrations/tests/test_cors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# coding: utf-8
from django.core.exceptions import ValidationError
from django.urls import reverse
from rest_framework.test import APITestCase

Expand Down Expand Up @@ -38,3 +39,21 @@ def test_cors_response_with_trusted_origin(self):
self.assertTrue(response.has_header(self.cors_response_header_name))
self.assertEqual(response[self.cors_response_header_name],
trusted_origin)

def test_origin_is_case_insensitive(self):
CorsModel.objects.create(cors='https://www.fsf.org')
response = self.client.get(
self.innocuous_url, headers={'origin': 'https://WWW.FSF.org'}
)
self.assertTrue(response.has_header(self.cors_response_header_name))

def test_default_http_port_rejected(self):
with self.assertRaises(ValidationError):
CorsModel(cors='http://example.com:80').full_clean()

def test_default_https_port_rejected(self):
with self.assertRaises(ValidationError):
CorsModel(cors='https://example.com:443').full_clean()

def test_non_standard_port_allowed(self):
CorsModel(cors='https://example.com:1234').full_clean()
Loading