Skip to content

Commit 71557f7

Browse files
jjurgens0pre-commit-ci[bot]adamchainzIssac Kelly
authored
Add Local Network Access support (#833)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Adam Johnson <[email protected]> Co-authored-by: Issac Kelly <[email protected]>
1 parent 404ddfa commit 71557f7

File tree

7 files changed

+73
-0
lines changed

7 files changed

+73
-0
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Changelog
1414

1515
* Add async support to the middleware, reducing overhead on async views.
1616

17+
* Add ``CORS_ALLOW_PRIVATE_NETWORK_ACCESS`` setting, which enables support for the Local Network Access draft specification.
18+
1719
3.14.0 (2023-02-25)
1820
-------------------
1921

README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,17 @@ Change the setting to ``'None'`` if you need to bypass this security restriction
300300

301301
.. _SESSION_COOKIE_SAMESITE: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-SESSION_COOKIE_SAMESITE
302302

303+
``CORS_ALLOW_PRIVATE_NETWORK: bool``
304+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
305+
306+
If ``True``, allow requests from sites on “public” IP to this server on a “private” IP.
307+
In such cases, browsers send an extra CORS header ``access-control-request-private-network``, for which ``OPTIONS`` responses must contain ``access-control-allow-private-network: true``.
308+
309+
Refer to:
310+
311+
* `Local Network Access <https://wicg.github.io/local-network-access/>`__, the W3C Community Draft specification.
312+
* `Private Network Access: introducing preflights <https://developer.chrome.com/blog/private-network-access-preflight/>`__, a blog post from the Google Chrome team.
313+
303314
CSRF Integration
304315
----------------
305316

src/corsheaders/checks.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ def check_settings(**kwargs: Any) -> list[CheckMessage]:
3838
Error("CORS_ALLOW_CREDENTIALS should be a bool.", id="corsheaders.E003")
3939
)
4040

41+
if not isinstance(conf.CORS_ALLOW_PRIVATE_NETWORK, bool):
42+
errors.append( # type: ignore [unreachable]
43+
Error(
44+
"CORS_ALLOW_PRIVATE_NETWORK should be a bool.",
45+
id="corsheaders.E015",
46+
)
47+
)
48+
4149
if (
4250
not isinstance(conf.CORS_PREFLIGHT_MAX_AGE, int)
4351
or conf.CORS_PREFLIGHT_MAX_AGE < 0

src/corsheaders/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ def CORS_ALLOW_METHODS(self) -> Sequence[str]:
3232
def CORS_ALLOW_CREDENTIALS(self) -> bool:
3333
return getattr(settings, "CORS_ALLOW_CREDENTIALS", False)
3434

35+
@property
36+
def CORS_ALLOW_PRIVATE_NETWORK(self) -> bool:
37+
return getattr(settings, "CORS_ALLOW_PRIVATE_NETWORK", False)
38+
3539
@property
3640
def CORS_PREFLIGHT_MAX_AGE(self) -> int:
3741
return getattr(settings, "CORS_PREFLIGHT_MAX_AGE", 86400)

src/corsheaders/middleware.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
ACCESS_CONTROL_ALLOW_HEADERS = "access-control-allow-headers"
2222
ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods"
2323
ACCESS_CONTROL_MAX_AGE = "access-control-max-age"
24+
ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "access-control-request-private-network"
25+
ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "access-control-allow-private-network"
2426

2527

2628
class CorsMiddleware:
@@ -129,6 +131,12 @@ def add_response_headers(
129131
if conf.CORS_PREFLIGHT_MAX_AGE:
130132
response[ACCESS_CONTROL_MAX_AGE] = str(conf.CORS_PREFLIGHT_MAX_AGE)
131133

134+
if (
135+
conf.CORS_ALLOW_PRIVATE_NETWORK
136+
and request.headers.get(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK) == "true"
137+
):
138+
response[ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK] = "true"
139+
132140
return response
133141

134142
def origin_found_in_white_lists(self, origin: str, url: SplitResult) -> bool:

tests/test_checks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def test_cors_allow_methods_non_string(self):
5252
def test_cors_allow_credentials_non_bool(self):
5353
self.check_error_codes(["corsheaders.E003"])
5454

55+
@override_settings(CORS_ALLOW_PRIVATE_NETWORK=object)
56+
def test_cors_allow_network_access_non_bool(self):
57+
self.check_error_codes(["corsheaders.E015"])
58+
5559
@override_settings(CORS_PREFLIGHT_MAX_AGE="10")
5660
def test_cors_preflight_max_age_non_integer(self):
5761
self.check_error_codes(["corsheaders.E004"])

tests/test_middleware.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from corsheaders.middleware import ACCESS_CONTROL_ALLOW_HEADERS
1212
from corsheaders.middleware import ACCESS_CONTROL_ALLOW_METHODS
1313
from corsheaders.middleware import ACCESS_CONTROL_ALLOW_ORIGIN
14+
from corsheaders.middleware import ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK
1415
from corsheaders.middleware import ACCESS_CONTROL_EXPOSE_HEADERS
1516
from corsheaders.middleware import ACCESS_CONTROL_MAX_AGE
1617
from tests.utils import prepend_middleware
@@ -94,6 +95,41 @@ def test_get_dont_allow_credentials(self):
9495
resp = self.client.get("/", HTTP_ORIGIN="https://example.com")
9596
assert ACCESS_CONTROL_ALLOW_CREDENTIALS not in resp
9697

98+
@override_settings(CORS_ALLOW_PRIVATE_NETWORK=True, CORS_ALLOW_ALL_ORIGINS=True)
99+
def test_allow_private_network_added_if_enabled_and_requested(self):
100+
resp = self.client.get(
101+
"/",
102+
HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK="true",
103+
HTTP_ORIGIN="http://example.com",
104+
)
105+
assert resp[ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK] == "true"
106+
107+
@override_settings(CORS_ALLOW_PRIVATE_NETWORK=True, CORS_ALLOW_ALL_ORIGINS=True)
108+
def test_allow_private_network_not_added_if_enabled_and_not_requested(self):
109+
resp = self.client.get("/", HTTP_ORIGIN="http://example.com")
110+
assert ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK not in resp
111+
112+
@override_settings(
113+
CORS_ALLOW_PRIVATE_NETWORK=True,
114+
CORS_ALLOWED_ORIGINS=["http://example.com"],
115+
)
116+
def test_allow_private_network_not_added_if_enabled_and_no_cors_origin(self):
117+
resp = self.client.get(
118+
"/",
119+
HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK="true",
120+
HTTP_ORIGIN="http://example.org",
121+
)
122+
assert ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK not in resp
123+
124+
@override_settings(CORS_ALLOW_PRIVATE_NETWORK=False, CORS_ALLOW_ALL_ORIGINS=True)
125+
def test_allow_private_network_not_added_if_disabled_and_requested(self):
126+
resp = self.client.get(
127+
"/",
128+
HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK="true",
129+
HTTP_ORIGIN="http://example.com",
130+
)
131+
assert ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK not in resp
132+
97133
@override_settings(
98134
CORS_ALLOW_HEADERS=["content-type"],
99135
CORS_ALLOW_METHODS=["GET", "OPTIONS"],

0 commit comments

Comments
 (0)