diff --git a/CHANGES/5319.bugfix.rst b/CHANGES/5319.bugfix.rst new file mode 100644 index 00000000000..4c9f8c7420f --- /dev/null +++ b/CHANGES/5319.bugfix.rst @@ -0,0 +1 @@ +Fixed a bug where server-side redirects (``HTTPFound`` and other ``HTTPMove`` subclasses) re-encoded percent-encoded characters (e.g. ``%3F`` for ``?``) in the ``Location`` header, which could break the redirected webserver when the URL contained a query string argument that was itself a URL. The ``Location`` header now preserves the original location string (CR/LF stripped for header-injection safety) while the ``location`` property still returns the parsed ``URL``. diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py index bd507a8813a..07df17045d3 100644 --- a/aiohttp/web_exceptions.py +++ b/aiohttp/web_exceptions.py @@ -231,12 +231,17 @@ def __init__( super().__init__( headers=headers, reason=reason, text=text, content_type=content_type ) - self._location = URL(location) - self.headers["Location"] = str(self.location) + self._parsed_location = URL(location) + # Preserve the original location string for the Location header so that + # percent-encoded characters (e.g. "%3F" for "?") are not re-encoded by + # yarl.URL (which decodes them and can break the redirected webserver). + # Only strip CR/LF to prevent request-smuggling-style header injection; + # the URL is still validated via URL(location) above. + self.headers["Location"] = str(location).replace("\r", "").replace("\n", "") @property def location(self) -> URL: - return self._location + return self._parsed_location class HTTPMultipleChoices(HTTPMove):