From c5b7ba3ed51e00f26a4ab8618295f8839808b2fc Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 25 Feb 2021 12:06:01 +0100 Subject: [PATCH] Merge branch 'ghsa-v6wp-4m6f-gcjg' into master This patch fixes an open redirect vulnerability bug in `aiohttp.web_middlewares.normalize_path_middleware` by making sure that there's at most one slash at the beginning of the `Location` header value. Refs: * https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html * https://github.com/aio-libs/aiohttp/security/advisories/GHSA-v6wp-4m6f-gcjg (cherry picked from commit 2545222a3853e31ace15d87ae0e2effb7da0c96b) --- CHANGES/5497.bugfix | 9 +++++++++ aiohttp/web_middlewares.py | 1 + tests/test_web_middleware.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 CHANGES/5497.bugfix diff --git a/CHANGES/5497.bugfix b/CHANGES/5497.bugfix new file mode 100644 index 00000000000..5cec6d75fe8 --- /dev/null +++ b/CHANGES/5497.bugfix @@ -0,0 +1,9 @@ +**(SECURITY BUG)** Started preventing open redirects in the +``aiohttp.web.normalize_path_middleware`` middleware. For +more details, see +https://github.com/aio-libs/aiohttp/security/advisories/GHSA-v6wp-4m6f-gcjg. + +Thanks to `Beast Glatisant `__ for +finding the first instance of this issue and `Jelmer Vernooij +`__ for reporting and tracking it down +in aiohttp. diff --git a/aiohttp/web_middlewares.py b/aiohttp/web_middlewares.py index 2625713b971..10455aa9dd3 100644 --- a/aiohttp/web_middlewares.py +++ b/aiohttp/web_middlewares.py @@ -102,6 +102,7 @@ async def impl(request: Request, handler: _Handler) -> StreamResponse: paths_to_check.append(merged_slashes[:-1]) for path in paths_to_check: + path = re.sub("^//+", "/", path) # SECURITY: GHSA-v6wp-4m6f-gcjg resolves, request = await _check_request_resolves(request, path) if resolves: raise redirect_class(request.raw_path + query) diff --git a/tests/test_web_middleware.py b/tests/test_web_middleware.py index 9b42ba3747e..dc1e2bac6fb 100644 --- a/tests/test_web_middleware.py +++ b/tests/test_web_middleware.py @@ -352,6 +352,38 @@ async def test_cannot_remove_and_add_slash(self) -> None: with pytest.raises(AssertionError): web.normalize_path_middleware(append_slash=True, remove_slash=True) + @pytest.mark.parametrize( + ["append_slash", "remove_slash"], + [ + (True, False), + (False, True), + (False, False), + ], + ) + async def test_open_redirects( + self, append_slash: bool, remove_slash: bool, aiohttp_client: Any + ) -> None: + async def handle(request: web.Request) -> web.StreamResponse: + pytest.fail( + msg="Security advisory 'GHSA-v6wp-4m6f-gcjg' test handler " + "matched unexpectedly", + pytrace=False, + ) + + app = web.Application( + middlewares=[ + web.normalize_path_middleware( + append_slash=append_slash, remove_slash=remove_slash + ) + ] + ) + app.add_routes([web.get("/", handle), web.get("/google.com", handle)]) + client = await aiohttp_client(app, server_kwargs={"skip_url_asserts": True}) + resp = await client.get("//google.com", allow_redirects=False) + assert resp.status == 308 + assert resp.headers["Location"] == "/google.com" + assert resp.url.query == URL("//google.com").query + async def test_old_style_middleware(loop, aiohttp_client) -> None: async def handler(request):