diff options
Diffstat (limited to 'meta-python/recipes-devtools/python/python3-aiohttp')
| -rw-r--r-- | meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-23334.patch | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-23334.patch b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-23334.patch new file mode 100644 index 0000000000..29909529aa --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-aiohttp/CVE-2024-23334.patch | |||
| @@ -0,0 +1,222 @@ | |||
| 1 | From 1c335944d6a8b1298baf179b7c0b3069f10c514b | ||
| 2 | From: Sam Bull <git@sambull.org> | ||
| 3 | Date: Sun Jan 28 18:13:06 2024 +0000 | ||
| 4 | Subject: [PATCH] python3-aiohttp: Validate static paths (#8079) | ||
| 5 | |||
| 6 | Co-authored-by: J. Nick Koston <nick@koston.org> | ||
| 7 | |||
| 8 | CVE: CVE-2024-23334 | ||
| 9 | |||
| 10 | Upstream-Status: Backport [https://github.com/aio-libs/aiohttp/commit/1c335944d6a8b1298baf179b7c0b3069f10c514b] | ||
| 11 | |||
| 12 | Signed-off-by: Rahul Janani Pandi <RahulJanani.Pandi@windriver.com> | ||
| 13 | --- | ||
| 14 | CHANGES/8079.bugfix.rst | 1 + | ||
| 15 | aiohttp/web_urldispatcher.py | 18 +++++-- | ||
| 16 | docs/web_advanced.rst | 16 ++++-- | ||
| 17 | docs/web_reference.rst | 12 +++-- | ||
| 18 | tests/test_web_urldispatcher.py | 91 +++++++++++++++++++++++++++++++++ | ||
| 19 | 5 files changed, 128 insertions(+), 10 deletions(-) | ||
| 20 | create mode 100644 CHANGES/8079.bugfix.rst | ||
| 21 | |||
| 22 | diff --git a/CHANGES/8079.bugfix.rst b/CHANGES/8079.bugfix.rst | ||
| 23 | new file mode 100644 | ||
| 24 | index 0000000..57bc8bf | ||
| 25 | --- /dev/null | ||
| 26 | +++ b/CHANGES/8079.bugfix.rst | ||
| 27 | @@ -0,0 +1 @@ | ||
| 28 | +Improved validation of paths for static resources -- by :user:`bdraco`. | ||
| 29 | diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py | ||
| 30 | index 5942e35..e8a8023 100644 | ||
| 31 | --- a/aiohttp/web_urldispatcher.py | ||
| 32 | +++ b/aiohttp/web_urldispatcher.py | ||
| 33 | @@ -593,9 +593,14 @@ class StaticResource(PrefixResource): | ||
| 34 | url = url / filename | ||
| 35 | |||
| 36 | if append_version: | ||
| 37 | + unresolved_path = self._directory.joinpath(filename) | ||
| 38 | try: | ||
| 39 | - filepath = self._directory.joinpath(filename).resolve() | ||
| 40 | - if not self._follow_symlinks: | ||
| 41 | + if self._follow_symlinks: | ||
| 42 | + normalized_path = Path(os.path.normpath(unresolved_path)) | ||
| 43 | + normalized_path.relative_to(self._directory) | ||
| 44 | + filepath = normalized_path.resolve() | ||
| 45 | + else: | ||
| 46 | + filepath = unresolved_path.resolve() | ||
| 47 | filepath.relative_to(self._directory) | ||
| 48 | except (ValueError, FileNotFoundError): | ||
| 49 | # ValueError for case when path point to symlink | ||
| 50 | @@ -660,8 +665,13 @@ class StaticResource(PrefixResource): | ||
| 51 | # /static/\\machine_name\c$ or /static/D:\path | ||
| 52 | # where the static dir is totally different | ||
| 53 | raise HTTPForbidden() | ||
| 54 | - filepath = self._directory.joinpath(filename).resolve() | ||
| 55 | - if not self._follow_symlinks: | ||
| 56 | + unresolved_path = self._directory.joinpath(filename) | ||
| 57 | + if self._follow_symlinks: | ||
| 58 | + normalized_path = Path(os.path.normpath(unresolved_path)) | ||
| 59 | + normalized_path.relative_to(self._directory) | ||
| 60 | + filepath = normalized_path.resolve() | ||
| 61 | + else: | ||
| 62 | + filepath = unresolved_path.resolve() | ||
| 63 | filepath.relative_to(self._directory) | ||
| 64 | except (ValueError, FileNotFoundError) as error: | ||
| 65 | # relatively safe | ||
| 66 | diff --git a/docs/web_advanced.rst b/docs/web_advanced.rst | ||
| 67 | index 3a98b78..5129397 100644 | ||
| 68 | --- a/docs/web_advanced.rst | ||
| 69 | +++ b/docs/web_advanced.rst | ||
| 70 | @@ -136,12 +136,22 @@ instead could be enabled with ``show_index`` parameter set to ``True``:: | ||
| 71 | |||
| 72 | web.static('/prefix', path_to_static_folder, show_index=True) | ||
| 73 | |||
| 74 | -When a symlink from the static directory is accessed, the server responses to | ||
| 75 | -client with ``HTTP/404 Not Found`` by default. To allow the server to follow | ||
| 76 | -symlinks, parameter ``follow_symlinks`` should be set to ``True``:: | ||
| 77 | +When a symlink that leads outside the static directory is accessed, the server | ||
| 78 | +responds to the client with ``HTTP/404 Not Found`` by default. To allow the server to | ||
| 79 | +follow symlinks that lead outside the static root, the parameter ``follow_symlinks`` | ||
| 80 | +should be set to ``True``:: | ||
| 81 | |||
| 82 | web.static('/prefix', path_to_static_folder, follow_symlinks=True) | ||
| 83 | |||
| 84 | +.. caution:: | ||
| 85 | + | ||
| 86 | + Enabling ``follow_symlinks`` can be a security risk, and may lead to | ||
| 87 | + a directory transversal attack. You do NOT need this option to follow symlinks | ||
| 88 | + which point to somewhere else within the static directory, this option is only | ||
| 89 | + used to break out of the security sandbox. Enabling this option is highly | ||
| 90 | + discouraged, and only expected to be used for edge cases in a local | ||
| 91 | + development setting where remote users do not have access to the server. | ||
| 92 | + | ||
| 93 | When you want to enable cache busting, | ||
| 94 | parameter ``append_version`` can be set to ``True`` | ||
| 95 | |||
| 96 | diff --git a/docs/web_reference.rst b/docs/web_reference.rst | ||
| 97 | index a156f47..b100676 100644 | ||
| 98 | --- a/docs/web_reference.rst | ||
| 99 | +++ b/docs/web_reference.rst | ||
| 100 | @@ -1836,9 +1836,15 @@ Router is any object that implements :class:`~aiohttp.abc.AbstractRouter` interf | ||
| 101 | by default it's not allowed and HTTP/403 will | ||
| 102 | be returned on directory access. | ||
| 103 | |||
| 104 | - :param bool follow_symlinks: flag for allowing to follow symlinks from | ||
| 105 | - a directory, by default it's not allowed and | ||
| 106 | - HTTP/404 will be returned on access. | ||
| 107 | + :param bool follow_symlinks: flag for allowing to follow symlinks that lead | ||
| 108 | + outside the static root directory, by default it's not allowed and | ||
| 109 | + HTTP/404 will be returned on access. Enabling ``follow_symlinks`` | ||
| 110 | + can be a security risk, and may lead to a directory transversal attack. | ||
| 111 | + You do NOT need this option to follow symlinks which point to somewhere | ||
| 112 | + else within the static directory, this option is only used to break out | ||
| 113 | + of the security sandbox. Enabling this option is highly discouraged, | ||
| 114 | + and only expected to be used for edge cases in a local development | ||
| 115 | + setting where remote users do not have access to the server. | ||
| 116 | |||
| 117 | :param bool append_version: flag for adding file version (hash) | ||
| 118 | to the url query string, this value will | ||
| 119 | diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py | ||
| 120 | index f24f451..f40f6a5 100644 | ||
| 121 | --- a/tests/test_web_urldispatcher.py | ||
| 122 | +++ b/tests/test_web_urldispatcher.py | ||
| 123 | @@ -123,6 +123,97 @@ async def test_follow_symlink(tmp_dir_path, aiohttp_client) -> None: | ||
| 124 | assert (await r.text()) == data | ||
| 125 | |||
| 126 | |||
| 127 | +async def test_follow_symlink_directory_traversal( | ||
| 128 | + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient | ||
| 129 | +) -> None: | ||
| 130 | + # Tests that follow_symlinks does not allow directory transversal | ||
| 131 | + data = "private" | ||
| 132 | + | ||
| 133 | + private_file = tmp_path / "private_file" | ||
| 134 | + private_file.write_text(data) | ||
| 135 | + | ||
| 136 | + safe_path = tmp_path / "safe_dir" | ||
| 137 | + safe_path.mkdir() | ||
| 138 | + | ||
| 139 | + app = web.Application() | ||
| 140 | + | ||
| 141 | + # Register global static route: | ||
| 142 | + app.router.add_static("/", str(safe_path), follow_symlinks=True) | ||
| 143 | + client = await aiohttp_client(app) | ||
| 144 | + | ||
| 145 | + await client.start_server() | ||
| 146 | + # We need to use a raw socket to test this, as the client will normalize | ||
| 147 | + # the path before sending it to the server. | ||
| 148 | + reader, writer = await asyncio.open_connection(client.host, client.port) | ||
| 149 | + writer.write(b"GET /../private_file HTTP/1.1\r\n\r\n") | ||
| 150 | + response = await reader.readuntil(b"\r\n\r\n") | ||
| 151 | + assert b"404 Not Found" in response | ||
| 152 | + writer.close() | ||
| 153 | + await writer.wait_closed() | ||
| 154 | + await client.close() | ||
| 155 | + | ||
| 156 | + | ||
| 157 | +async def test_follow_symlink_directory_traversal_after_normalization( | ||
| 158 | + tmp_path: pathlib.Path, aiohttp_client: AiohttpClient | ||
| 159 | +) -> None: | ||
| 160 | + # Tests that follow_symlinks does not allow directory transversal | ||
| 161 | + # after normalization | ||
| 162 | + # | ||
| 163 | + # Directory structure | ||
| 164 | + # |-- secret_dir | ||
| 165 | + # | |-- private_file (should never be accessible) | ||
| 166 | + # | |-- symlink_target_dir | ||
| 167 | + # | |-- symlink_target_file (should be accessible via the my_symlink symlink) | ||
| 168 | + # | |-- sandbox_dir | ||
| 169 | + # | |-- my_symlink -> symlink_target_dir | ||
| 170 | + # | ||
| 171 | + secret_path = tmp_path / "secret_dir" | ||
| 172 | + secret_path.mkdir() | ||
| 173 | + | ||
| 174 | + # This file is below the symlink target and should not be reachable | ||
| 175 | + private_file = secret_path / "private_file" | ||
| 176 | + private_file.write_text("private") | ||
| 177 | + | ||
| 178 | + symlink_target_path = secret_path / "symlink_target_dir" | ||
| 179 | + symlink_target_path.mkdir() | ||
| 180 | + | ||
| 181 | + sandbox_path = symlink_target_path / "sandbox_dir" | ||
| 182 | + sandbox_path.mkdir() | ||
| 183 | + | ||
| 184 | + # This file should be reachable via the symlink | ||
| 185 | + symlink_target_file = symlink_target_path / "symlink_target_file" | ||
| 186 | + symlink_target_file.write_text("readable") | ||
| 187 | + | ||
| 188 | + my_symlink_path = sandbox_path / "my_symlink" | ||
| 189 | + pathlib.Path(str(my_symlink_path)).symlink_to(str(symlink_target_path), True) | ||
| 190 | + | ||
| 191 | + app = web.Application() | ||
| 192 | + | ||
| 193 | + # Register global static route: | ||
| 194 | + app.router.add_static("/", str(sandbox_path), follow_symlinks=True) | ||
| 195 | + client = await aiohttp_client(app) | ||
| 196 | + | ||
| 197 | + await client.start_server() | ||
| 198 | + # We need to use a raw socket to test this, as the client will normalize | ||
| 199 | + # the path before sending it to the server. | ||
| 200 | + reader, writer = await asyncio.open_connection(client.host, client.port) | ||
| 201 | + writer.write(b"GET /my_symlink/../private_file HTTP/1.1\r\n\r\n") | ||
| 202 | + response = await reader.readuntil(b"\r\n\r\n") | ||
| 203 | + assert b"404 Not Found" in response | ||
| 204 | + writer.close() | ||
| 205 | + await writer.wait_closed() | ||
| 206 | + | ||
| 207 | + reader, writer = await asyncio.open_connection(client.host, client.port) | ||
| 208 | + writer.write(b"GET /my_symlink/symlink_target_file HTTP/1.1\r\n\r\n") | ||
| 209 | + response = await reader.readuntil(b"\r\n\r\n") | ||
| 210 | + assert b"200 OK" in response | ||
| 211 | + response = await reader.readuntil(b"readable") | ||
| 212 | + assert response == b"readable" | ||
| 213 | + writer.close() | ||
| 214 | + await writer.wait_closed() | ||
| 215 | + await client.close() | ||
| 216 | + | ||
| 217 | + | ||
| 218 | @pytest.mark.parametrize( | ||
| 219 | "dir_name,filename,data", | ||
| 220 | [ | ||
| 221 | -- | ||
| 222 | 2.40.0 | ||
