Repository: jschneier/django-storages

Issue:
I can't upload image with dropbox SuspiciousFileOperation 
settings.py
```
STATICFILES_STORAGE = 'storages.backends.dropbox.DropBoxStorage'
DEFAULT_FILE_STORAGE = 'storages.backends.dropbox.DropBoxStorage'
DROPBOX_OAUTH2_TOKEN = 'xxx-xxx'
DROPBOX_ROOT_PATH = '/TKYK/'
```
my models.py
```
def get_image_filename(instance, filename):
    fpath = pathlib.Path(filename)
    new_fname = str(uuid.uuid1()) # uuid1 -> uuid + timestamps
    return f"patient/{new_fname}{fpath.suffix}"

class Image (models.Model):
    me = models.ForeignKey(me, on_delete=models.CASCADE)
    images_data = models.FileField(upload_to=get_image_filename)
```

error when upload via django admin site
![image](https://user-images.githubusercontent.com/26823364/153148043-456f43fd-93fe-472c-8ce1-ff7f1d9f4b09.png)

But I see the image already uploaded to dropbox
![image](https://user-images.githubusercontent.com/26823364/153148453-a90080d4-a1b6-45db-a260-394e28917003.png)



Discussion:
Cheating:
nano /usr/local/lib/python3.8/dist-packages/django/core/files/utils.py

add # from line 18 - 21
```
        # if path.is_absolute() or '..' in path.parts:
        #    raise SuspiciousFileOperation(
        #       "Detected path traversal attempt in '%s'" % name
        #  )
```
@churongcon this appears to be related to a change in Django. What happens if you change `DROP_ROOT_PATH` to be `'TKYK/'`??
I am having the same issue I am using django-dbbackup to send files to dropbox. 
Django=3.2.11
`django.core.exceptions.SuspiciousFileOperation: Detected path traversal attempt in '/default-ubuntu-2022-02-10-114225.psql'`
> @churongcon this appears to be related to a change in Django. What happens if you change `DROP_ROOT_PATH` to be `'TKYK/'`??

same issues with lastest Django (3.2.12) version  

I used django 3.1 before it working fine
I think this issue occur because Dropbox get Full Path as name but django need only filename+extension
> > @churongcon this appears to be related to a change in Django. What happens if you change `DROP_ROOT_PATH` to be `'TKYK/'`??
> 
> same issues with lastest Django (3.2.12) version
> 
> I used django 3.1 before it working fine

Interesting. Yes this appears to be related to this Django change:

https://github.com/django/django/commit/b55699968fc9ee985384c64e37f6cc74a0a23683
If I understand that commit correctly we are receiving this because `django-storage` is trying to access a file using the absolute path.

I tried to use different values for `DROPBOX_ROOT_PATH`  like `BASE_DIR` and `MEDIA_DIR` none of it worked. 

Did someone manage to make it work? I see that @churongcon was able to make it work by commenting out the code in Django itself but it's not a doable solution. 


I have the same problem, opened a [Stackoverflow post](https://stackoverflow.com/questions/71294243/django-storages-dropbox-suspiciousfileoperation) before finding this thread. Like @TheAbhijeet, I tried different configurations, but no one worked.
Edit : I'm using Django(4.0) as @abiatarfestus
I'm facing the exact same problem in Django(4.0). I hope someone finds a solution.
Have you tried to use `os.path.join` to create the path?
like this, using @churongcon example
```py
def create_path(instance, filename):
    fpath = pathlib.Path(filename)
    new_fname = str(uuid.uuid1()) # uuid1 -> uuid + timestamps
    return os.path.join(
        "patient",
        f"{new_fname}{fpath.suffix}"
    )

class Image (models.Model):
    me = models.ForeignKey(me, on_delete=models.CASCADE)
    images_data = models.FileField(upload_to=create_path)
```
+1 same problem for Django 4.0.3
I needed to get around this, so FWIW, here is a monkey patch to django that seems to work (not sure if this has any unintended consequences):

```py
import os
import pathlib

from django.core.exceptions import SuspiciousFileOperation
from django.core.files import storage


# monkey patch django for https://github.com/jschneier/django-storages/issues/1109
def validate_file_name(name, allow_relative_path=False):
    # Remove potentially dangerous names
    if os.path.basename(name) in {'', '.', '..'}:
        raise SuspiciousFileOperation('Could not derive file name from "%s"' % name)

    if allow_relative_path:
        # Use PurePosixPath() because this branch is checked only in
        # FileField.generate_filename() where all file paths are expected to be
        # Unix style (with forward slashes).
        path = pathlib.PurePosixPath(name)
        # if path.is_absolute() or '..' in path.parts:  # <----------- problem
        if '..' in path.parts:
            raise SuspiciousFileOperation(
                'Detected path traversal attempt in "%s"' % name
            )
    elif name != os.path.basename(name):
        raise SuspiciousFileOperation('File name "%s" includes path elements' % name)

    return name

storage.validate_file_name = validate_file_name

```

> I needed to get around this, so FWIW, here is a monkey patch to django that seems to work (not sure if this has any unintended consequences):
> 
> ```python
> import os
> import pathlib
> 
> from django.core.exceptions import SuspiciousFileOperation
> from django.core.files import storage
> 
> 
> # monkey patch django for https://github.com/jschneier/django-storages/issues/1109
> def validate_file_name(name, allow_relative_path=False):
>     # Remove potentially dangerous names
>     if os.path.basename(name) in {'', '.', '..'}:
>         raise SuspiciousFileOperation('Could not derive file name from "%s"' % name)
> 
>     if allow_relative_path:
>         # Use PurePosixPath() because this branch is checked only in
>         # FileField.generate_filename() where all file paths are expected to be
>         # Unix style (with forward slashes).
>         path = pathlib.PurePosixPath(name)
>         # if path.is_absolute() or '..' in path.parts:  # <----------- problem
>         if '..' in path.parts:
>             raise SuspiciousFileOperation(
>                 'Detected path traversal attempt in "%s"' % name
>             )
>     elif name != os.path.basename(name):
>         raise SuspiciousFileOperation('File name "%s" includes path elements' % name)
> 
>     return name
> 
> storage.validate_file_name = validate_file_name
> ```

I have the same error in my django/wagtail project but I'm running it in docker.. 
Where do I copy paste @banesullivan patch, help I'm a bit confused ??
> Where do I copy paste @banesullivan patch, help I'm a bit confused ??

Since it's a monkey-patch, you can copy/paste this anywhere in your django project that is actively imported. [Wikipedia](https://en.wikipedia.org/wiki/Monkey_patch) has a simple example of monkey patching for reference

Resolving patch:
diff --git a/storages/backends/dropbox.py b/storages/backends/dropbox.py
--- a/storages/backends/dropbox.py
+++ b/storages/backends/dropbox.py
@@ -170,7 +170,9 @@ def _save(self, name, content):
         else:
             self._chunked_upload(content, self._full_path(name))
         content.close()
-        return name
+        # .save() validates the filename isn't absolute but Dropbox requires an absolute filename.
+        # Work with the absolute name internally but strip it off before passing up-the-chain.
+        return name.lstrip(self.root_path)
 
     def _chunked_upload(self, content, dest_path):
         upload_session = self.client.files_upload_session_start(


Test patch:
diff --git a/tests/test_dropbox.py b/tests/test_dropbox.py
--- a/tests/test_dropbox.py
+++ b/tests/test_dropbox.py
@@ -130,11 +130,12 @@ def test_open(self, *args):
         obj = self.storage._open('foo')
         self.assertIsInstance(obj, File)
 
-    @mock.patch('dropbox.Dropbox.files_upload',
-                return_value='foo')
+    @mock.patch('dropbox.Dropbox.files_upload', return_value='foo')
+    @mock.patch('dropbox.Dropbox.files_get_metadata', return_value=None)
     def test_save(self, files_upload, *args):
-        self.storage._save('foo', File(io.BytesIO(b'bar'), 'foo'))
+        name = self.storage.save('foo', File(io.BytesIO(b'bar'), 'foo'))
         self.assertTrue(files_upload.called)
+        self.assertEqual(name, 'foo')
 
     @mock.patch('dropbox.Dropbox.files_upload')
     @mock.patch('dropbox.Dropbox.files_upload_session_finish')
@@ -192,6 +193,13 @@ def test_jailed(self, *args):
         self.assertFalse(dirs)
         self.assertFalse(files)
 
+    @mock.patch('dropbox.Dropbox.files_upload', return_value='foo')
+    @mock.patch('dropbox.Dropbox.files_get_metadata', return_value=None)
+    def test_saves(self, *args):
+        self.storage = dropbox.DropBoxStorage('foo', root_path='/bar')
+        name = self.storage.save('xyz', File(io.BytesIO(b'abc'), 'def'))
+        self.assertEqual(name, 'xyz')
+
     def test_suspicious(self, *args):
         self.storage = dropbox.DropBoxStorage('foo', root_path='/bar')
         with self.assertRaises((SuspiciousFileOperation, ValueError)):