@@ -130,6 +130,97 @@ async def test_follow_symlink(
130
130
assert (await r .text ()) == data
131
131
132
132
133
+ async def test_follow_symlink_directory_traversal (
134
+ tmp_path : pathlib .Path , aiohttp_client : AiohttpClient
135
+ ) -> None :
136
+ # Tests that follow_symlinks does not allow directory transversal
137
+ data = "private"
138
+
139
+ private_file = tmp_path / "private_file"
140
+ private_file .write_text (data )
141
+
142
+ safe_path = tmp_path / "safe_dir"
143
+ safe_path .mkdir ()
144
+
145
+ app = web .Application ()
146
+
147
+ # Register global static route:
148
+ app .router .add_static ("/" , str (safe_path ), follow_symlinks = True )
149
+ client = await aiohttp_client (app )
150
+
151
+ await client .start_server ()
152
+ # We need to use a raw socket to test this, as the client will normalize
153
+ # the path before sending it to the server.
154
+ reader , writer = await asyncio .open_connection (client .host , client .port )
155
+ writer .write (b"GET /../private_file HTTP/1.1\r \n \r \n " )
156
+ response = await reader .readuntil (b"\r \n \r \n " )
157
+ assert b"404 Not Found" in response
158
+ writer .close ()
159
+ await writer .wait_closed ()
160
+ await client .close ()
161
+
162
+
163
+ async def test_follow_symlink_directory_traversal_after_normalization (
164
+ tmp_path : pathlib .Path , aiohttp_client : AiohttpClient
165
+ ) -> None :
166
+ # Tests that follow_symlinks does not allow directory transversal
167
+ # after normalization
168
+ #
169
+ # Directory structure
170
+ # |-- secret_dir
171
+ # | |-- private_file (should never be accessible)
172
+ # | |-- symlink_target_dir
173
+ # | |-- symlink_target_file (should be accessible via the my_symlink symlink)
174
+ # | |-- sandbox_dir
175
+ # | |-- my_symlink -> symlink_target_dir
176
+ #
177
+ secret_path = tmp_path / "secret_dir"
178
+ secret_path .mkdir ()
179
+
180
+ # This file is below the symlink target and should not be reachable
181
+ private_file = secret_path / "private_file"
182
+ private_file .write_text ("private" )
183
+
184
+ symlink_target_path = secret_path / "symlink_target_dir"
185
+ symlink_target_path .mkdir ()
186
+
187
+ sandbox_path = symlink_target_path / "sandbox_dir"
188
+ sandbox_path .mkdir ()
189
+
190
+ # This file should be reachable via the symlink
191
+ symlink_target_file = symlink_target_path / "symlink_target_file"
192
+ symlink_target_file .write_text ("readable" )
193
+
194
+ my_symlink_path = sandbox_path / "my_symlink"
195
+ pathlib .Path (str (my_symlink_path )).symlink_to (str (symlink_target_path ), True )
196
+
197
+ app = web .Application ()
198
+
199
+ # Register global static route:
200
+ app .router .add_static ("/" , str (sandbox_path ), follow_symlinks = True )
201
+ client = await aiohttp_client (app )
202
+
203
+ await client .start_server ()
204
+ # We need to use a raw socket to test this, as the client will normalize
205
+ # the path before sending it to the server.
206
+ reader , writer = await asyncio .open_connection (client .host , client .port )
207
+ writer .write (b"GET /my_symlink/../private_file HTTP/1.1\r \n \r \n " )
208
+ response = await reader .readuntil (b"\r \n \r \n " )
209
+ assert b"404 Not Found" in response
210
+ writer .close ()
211
+ await writer .wait_closed ()
212
+
213
+ reader , writer = await asyncio .open_connection (client .host , client .port )
214
+ writer .write (b"GET /my_symlink/symlink_target_file HTTP/1.1\r \n \r \n " )
215
+ response = await reader .readuntil (b"\r \n \r \n " )
216
+ assert b"200 OK" in response
217
+ response = await reader .readuntil (b"readable" )
218
+ assert response == b"readable"
219
+ writer .close ()
220
+ await writer .wait_closed ()
221
+ await client .close ()
222
+
223
+
133
224
@pytest .mark .parametrize (
134
225
"dir_name,filename,data" ,
135
226
[
0 commit comments