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