diff --git a/CHANGES.rst b/CHANGES.rst index 7aa0479b4..9c3afc517 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -117,7 +117,9 @@ Unreleased and in some tests. MD5 is not available in some environments, such as FIPS 140. This may invalidate some caches since the ETag will be different. :issue:`1897` - +- Add ``Cross-Origin-Opener-Policy`` and + ``Cross-Origin-Embedder-Policy`` response header properties. + :pr:`2008` Version 1.0.2 ------------- diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py index 880e297e5..8db405a0e 100644 --- a/src/werkzeug/http.py +++ b/src/werkzeug/http.py @@ -6,6 +6,7 @@ from datetime import datetime from datetime import timedelta from email.utils import parsedate_tz +from enum import Enum from hashlib import sha1 from time import gmtime from time import struct_time @@ -172,6 +173,21 @@ } +class COEP(Enum): + """Cross Origin Embedder Policies""" + + UNSAFE_NONE = "unsafe-none" + REQUIRE_CORP = "require-corp" + + +class COOP(Enum): + """Cross Origin Opener Policies""" + + UNSAFE_NONE = "unsafe-none" + SAME_ORIGIN_ALLOW_POPUPS = "same-origin-allow-popups" + SAME_ORIGIN = "same-origin" + + def quote_header_value( value: t.Union[str, int], extra_chars: str = "", allow_token: bool = True ) -> str: diff --git a/src/werkzeug/wrappers/response.py b/src/werkzeug/wrappers/response.py index 94207f12e..c956abc00 100644 --- a/src/werkzeug/wrappers/response.py +++ b/src/werkzeug/wrappers/response.py @@ -22,6 +22,8 @@ from werkzeug.datastructures import ContentRange from werkzeug.datastructures import ResponseCacheControl from werkzeug.datastructures import WWWAuthenticate +from werkzeug.http import COEP +from werkzeug.http import COOP from werkzeug.http import dump_age from werkzeug.http import dump_csp_header from werkzeug.http import dump_header @@ -1376,6 +1378,25 @@ def access_control_allow_credentials(self, value: t.Optional[bool]) -> None: doc="The maximum age in seconds the access control settings can be cached for.", ) + cross_origin_opener_policy = header_property[COOP]( + "Cross-Origin-Opener-Policy", + load_func=lambda value: COOP(value), + dump_func=lambda value: value.value, + default=COOP.UNSAFE_NONE, + doc="""Allows control over sharing of browsing context group with cross-origin + documents. Values must be a member of the :class:`werkzeug.http.COOP` enum.""", + ) + + cross_origin_embedder_policy = header_property[COEP]( + "Cross-Origin-Embedder-Policy", + load_func=lambda value: COEP(value), + dump_func=lambda value: value.value, + default=COEP.UNSAFE_NONE, + doc="""Prevents a document from loading any cross-origin resources that do not + explicitly grant the document permission. Values must be a member of the + :class:`werkzeug.http.COEP` enum.""", + ) + class ResponseStream: """A file descriptor like object used by the :class:`ResponseStreamMixin` to diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index c3df604f9..34d756c93 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -22,6 +22,8 @@ from werkzeug.exceptions import BadRequest from werkzeug.exceptions import RequestedRangeNotSatisfiable from werkzeug.exceptions import SecurityError +from werkzeug.http import COEP +from werkzeug.http import COOP from werkzeug.http import generate_etag from werkzeug.test import Client from werkzeug.test import create_environ @@ -1576,3 +1578,17 @@ def test_check_base_deprecated(): def test_response_freeze_no_etag_deprecated(): with pytest.raises(DeprecationWarning, match="no_etag"): Response("Hello, World!").freeze(no_etag=True) + + +def test_response_coop(): + response = wrappers.Response("Hello World") + assert response.cross_origin_opener_policy is COOP.UNSAFE_NONE + response.cross_origin_opener_policy = COOP.SAME_ORIGIN + assert response.headers["Cross-Origin-Opener-Policy"] == "same-origin" + + +def test_response_coep(): + response = wrappers.Response("Hello World") + assert response.cross_origin_embedder_policy is COEP.UNSAFE_NONE + response.cross_origin_embedder_policy = COEP.REQUIRE_CORP + assert response.headers["Cross-Origin-Embedder-Policy"] == "require-corp"