Skip to content

Commit beab3dd

Browse files
committed
Improve asset file retrieval
1 parent 17ee0c6 commit beab3dd

3 files changed

Lines changed: 53 additions & 7 deletions

File tree

geonode/assets/local.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def create_response(
256256
return HttpResponse("Asset does not contain any data", status=500)
257257

258258
if len(asset.location) > 1:
259-
logger.warning("TODO: Asset contains more than one file. Download needs to be implemented")
259+
logger.warning("TODO: Asset contains more than one file. Only first file will be returned")
260260

261261
file0 = asset.location[0]
262262
if not path: # use the file definition
@@ -266,10 +266,6 @@ def create_response(
266266
localfile = file0
267267

268268
else: # a specific file is requested
269-
if "/../" in path: # we may want to improve fraudolent request detection
270-
logger.warning(f"Tentative path traversal for asset {asset.id}")
271-
return HttpResponse(f"File not found for asset {asset.id}", status=400)
272-
273269
if os.path.isfile(file0):
274270
dir0 = os.path.dirname(file0)
275271
elif os.path.isdir(file0):
@@ -280,6 +276,13 @@ def create_response(
280276
localfile = os.path.join(dir0, path)
281277
logger.debug(f"Requested path {dir0} + {path}")
282278

279+
# check the requested file is within the asset's directory
280+
localfile = os.path.realpath(localfile)
281+
realassetpath = os.path.realpath(dir0)
282+
if not localfile.startswith(realassetpath):
283+
logger.error(f"Tentative path traversal for asset {asset.id} on path [{path}]")
284+
return HttpResponse(f"File not found for asset {asset.id}", status=400)
285+
283286
if os.path.isfile(localfile):
284287
filename = os.path.basename(localfile)
285288
orig_base, ext = os.path.splitext(filename)

geonode/assets/tests.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,49 @@ def test_download_with_attachment_for_anonymous(self):
415415
if resource:
416416
resource.delete()
417417

418+
def test_cross_download(self):
419+
420+
admin, _ = get_user_model().objects.get_or_create(username="admin")
421+
user1, _ = get_user_model().objects.get_or_create(username="user1")
422+
user2, _ = get_user_model().objects.get_or_create(username="user2")
423+
424+
asset_handler = asset_handler_registry.get_default_handler()
425+
426+
assets = []
427+
for user, file in ((user1, ONE_JSON), (user2, TWO_JSON)):
428+
asset1 = asset_handler.create(
429+
title="Test Asset1",
430+
description="Description of test asset",
431+
type="NeverMind",
432+
owner=user1,
433+
files=[file],
434+
clone_files=True,
435+
)
436+
asset1.save()
437+
self.assertIsInstance(asset1, LocalAsset)
438+
assets.append(asset1)
439+
440+
# check that user1 can access file1
441+
self.client.force_login(user1)
442+
443+
args = {"pk": assets[0].pk, "path": "one.json"}
444+
url = reverse("assets-link", kwargs=args)
445+
response = self.client.get(url)
446+
self.assertEqual(response.status_code, 200)
447+
448+
# check that user1 can NOT access file2 using asset1 link/download
449+
a2path = assets[1].location[0]
450+
logger.debug(f"Asset path {a2path}")
451+
a2dir = a2path.split("/")[-2]
452+
logger.debug(f"Asset dir {a2dir}")
453+
forged_path = f"../{a2dir}/two.json" # path will be automatically urlencoded into "%2e%2e%2f{a2dir}%2ftwo.json"
454+
455+
args = {"pk": assets[0].pk, "path": forged_path}
456+
url = reverse("assets-link", kwargs=args)
457+
logger.debug(f"Reverse link URL is {url}")
458+
response = self.client.get(url)
459+
self.assertEqual(response.status_code, 400)
460+
418461
def _setup_test(self, u, _file=ONE_JSON):
419462
asset_handler = asset_handler_registry.get_default_handler()
420463
asset = asset_handler.create(

geonode/assets/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def list(self, request, *args, **kwargs):
6363
queryset = self.filter_queryset(self.get_queryset())
6464

6565
user = request.user
66-
is_admin = user.is_superuser if user and user.is_authenticated else False
66+
is_admin = user and user.is_authenticated and user.is_superuser
6767

6868
if is_admin:
6969
pass
@@ -104,5 +104,5 @@ def download(self, request, pk=None, path=None, *args, **kwargs):
104104
methods=["get"],
105105
)
106106
def link(self, request, pk=None, path=None, *args, **kwargs):
107-
logger.warning(f"REQUESTED ASSET LINK FOR PK:{pk} PATH:{path}")
107+
logger.debug(f"REQUESTED ASSET LINK FOR PK:{pk} PATH:{path}")
108108
return self._get_file(request, pk, attachment=False, path=path)

0 commit comments

Comments
 (0)