Updated - use correct CVE identifiers, as they are now available - havn't 
noticed before.

On Samstag, 11. Juli 2026 14:28 you wrote:
> Package: release.debian.org
> Severity: normal
> Tags: trixie
> X-Debbugs-Cc: [email protected], Debian Security Team
> <[email protected]>, [email protected], [email protected] Control:
> affects -1 + src:onionshare
> User: [email protected]
> Usertags: pu
> 
> [ Reason ]
> Fixes two upstream advisories:
> 
> https://bugs.debian.org/1139716
> -> https://security-tracker.debian.org/tracker/TEMP-1139716-6892B6
> ->
> https://github.com/onionshare/onionshare/security/advisories/GHSA-v833-3823
> -cmhp
> 
> https://bugs.debian.org/1139717
> -> https://security-tracker.debian.org/tracker/TEMP-1139717-36B614
> ->
> https://github.com/onionshare/onionshare/security/advisories/GHSA-22p9-r2f5
> -22mf
> 
> Both issues are fixed with 2.6.4-1 in Forky already. Unfortunately 2.6.4
> updated unrelated things too. So I backported the relevant patches to fix
> the two issues including the additional unit tests to 2.6.3 for Trixie.
> 
> The securtiy team already marked those two issues as no-dsa, so the way
> to fix it is via p-u:
> https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1139716#20
> 
> [ Impact ]
> The user will still be affected by this issues:
> - Allows unintended disclosure of local files.
> - Allow a remote sender to create directories on your device.
> 
> [ Tests ]
> Build the package and let the autopkgtests run, that has the additional
> tests added.
> 
> [ Risks ]
> 
> The patches come from upstream and could applied without changes on
> 2.6.3. The affected files are not changed by other commits in the diff
> 2.6.3..2.6.4. So a hidden side-effect is unlikely.
> The patches itself are small and focused. The existing tests already
> tests a big portion of the usages and the additional tests care about
> the two issues.
> IMO the risk to introduce new bugs is small.
> 
> [ Checklist ]
>   [x] *all* changes are documented in the d/changelog
>   [x] I reviewed all changes and I approve them
>   [x] attach debdiff against the package in stable
>   [x] the issue is verified as fixed in unstable
> 
> [ Changes ]
> * Backport six upstream commits
> * Updated debian branch in debian/gbp.conf


diff -Nru onionshare-2.6.3/debian/changelog onionshare-2.6.3/debian/changelog
--- onionshare-2.6.3/debian/changelog	2025-03-05 02:53:15.000000000 +0100
+++ onionshare-2.6.3/debian/changelog	2026-07-11 19:28:08.000000000 +0200
@@ -1,3 +1,25 @@
+onionshare (2.6.3-1+deb13u2) trixie; urgency=medium
+
+  * Use CVE identifiers.
+
+ -- Sandro Knauß <[email protected]>  Sat, 11 Jul 2026 19:28:08 +0200
+
+onionshare (2.6.3-1+deb13u1) trixie; urgency=medium
+
+  * Update debian branch in gbp.conf.
+  * Fixes CVE-2026-54707. (Closes: #1139716)
+    Backport upstream patches from 2.6.4:
+    - a090e97193efc91fbeac9dace7793ea568b83cf5
+    - e4ed559908b55cf41b993ed3d02052911025785b
+    - 1d76494f5d819adc4c69f08f3842d564a425aa89
+  * Fixes CVE-2026-54706. (Closes: #1139717)
+    Backport upstream patches from 2.6.4:
+    - 48f31cfac077fcc9c04c67c2a6dbf87d956f5eec
+    - 96827bdd0580bd34b921a7b269198f65434178d8
+    - d0697fa3d05a193138c6052422c313612301d98a
+
+ -- Sandro Knauß <[email protected]>  Sat, 11 Jul 2026 13:34:34 +0200
+
 onionshare (2.6.3-1) unstable; urgency=medium
 
   * New upstream release (Closes: #1070349, #1083519).
diff -Nru onionshare-2.6.3/debian/gbp.conf onionshare-2.6.3/debian/gbp.conf
--- onionshare-2.6.3/debian/gbp.conf	2025-03-05 02:53:15.000000000 +0100
+++ onionshare-2.6.3/debian/gbp.conf	2026-07-11 12:43:13.000000000 +0200
@@ -1,4 +1,4 @@
 [DEFAULT]
 pristine-tar = True
-debian-branch = debian/sid
+debian-branch = debian/trixie
 upstream-branch = master
diff -Nru onionshare-2.6.3/debian/.gitignore onionshare-2.6.3/debian/.gitignore
--- onionshare-2.6.3/debian/.gitignore	2025-03-05 02:53:15.000000000 +0100
+++ onionshare-2.6.3/debian/.gitignore	1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-files
diff -Nru onionshare-2.6.3/debian/patches/0005-Prevent-writing-files-in-Receive-mode-when-file-uplo.patch onionshare-2.6.3/debian/patches/0005-Prevent-writing-files-in-Receive-mode-when-file-uplo.patch
--- onionshare-2.6.3/debian/patches/0005-Prevent-writing-files-in-Receive-mode-when-file-uplo.patch	1970-01-01 01:00:00.000000000 +0100
+++ onionshare-2.6.3/debian/patches/0005-Prevent-writing-files-in-Receive-mode-when-file-uplo.patch	2026-07-11 13:30:40.000000000 +0200
@@ -0,0 +1,229 @@
+From: Miguel Jacq <[email protected]>
+Date: Sat, 30 May 2026 16:35:24 +1000
+Subject: Prevent writing files in Receive mode when file uploads are disabled
+
+Origin: upstream, commit:a090e97193efc91fbeac9dace7793ea568b83cf5
+Bug: https://github.com/onionshare/onionshare/security/advisories/GHSA-v833-3823-cmhp
+Bug-Debian: https://bugs.debian.org/1139716
+---
+ cli/onionshare_cli/web/receive_mode.py | 61 +++++++++++++++++++++----
+ desktop/tests/test_gui_receive.py      | 82 +++++++++++++++++++++++++++++++++-
+ 2 files changed, 133 insertions(+), 10 deletions(-)
+
+diff --git a/cli/onionshare_cli/web/receive_mode.py b/cli/onionshare_cli/web/receive_mode.py
+index a25f82a..a5a208a 100644
+--- a/cli/onionshare_cli/web/receive_mode.py
++++ b/cli/onionshare_cli/web/receive_mode.py
+@@ -22,6 +22,7 @@ import os
+ import tempfile
+ import json
+ import requests
++import io
+ from datetime import datetime
+ from flask import Request, request, render_template, make_response, flash, redirect
+ from werkzeug.utils import secure_filename
+@@ -91,10 +92,12 @@ class ReceiveModeWeb:
+                 static_url_path=self.web.static_url_path,
+                 disable_text=self.web.settings.get("receive", "disable_text"),
+                 disable_files=self.web.settings.get("receive", "disable_files"),
+-                title=self.web.settings.get("general", "title")
++                title=self.web.settings.get("general", "title"),
+             )
+ 
+-        @self.web.app.route("/upload", methods=["POST"], provide_automatic_options=False)
++        @self.web.app.route(
++            "/upload", methods=["POST"], provide_automatic_options=False
++        )
+         def upload(ajax=False):
+             """
+             Handle the upload files POST request, though at this point, the files have
+@@ -133,6 +136,18 @@ class ReceiveModeWeb:
+                         print(f"Received: {local_path}")
+ 
+                 files_received = len(filenames)
++            else:
++                # Check if files were submitted when disabled
++                files = request.files.getlist("file[]")
++                if files and any(f.filename != "" for f in files):
++                    request.upload_error = True
++                    if ajax:
++                        return json.dumps(
++                            {"error_flashes": ["File uploads are not allowed"]}
++                        )
++                    else:
++                        flash("File uploads are not allowed", "error")
++                        return redirect("/")
+ 
+             # Send webhook if configured
+             if (
+@@ -194,10 +209,10 @@ class ReceiveModeWeb:
+                 if files_received > 0:
+                     msg = f"Uploaded {files_msg}"
+                 else:
+-                   if not self.web.settings.get("receive", "disable_text"):
+-                       msg = "Nothing submitted or message was too long (> 524288 characters)"
+-                   else:
+-                       msg = "Nothing submitted"
++                    if not self.web.settings.get("receive", "disable_text"):
++                        msg = "Nothing submitted or message was too long (> 524288 characters)"
++                    else:
++                        msg = "Nothing submitted"
+ 
+             if ajax:
+                 info_flashes.append(msg)
+@@ -228,7 +243,9 @@ class ReceiveModeWeb:
+                         title=self.web.settings.get("general", "title"),
+                     )
+ 
+-        @self.web.app.route("/upload-ajax", methods=["POST"], provide_automatic_options=False)
++        @self.web.app.route(
++            "/upload-ajax", methods=["POST"], provide_automatic_options=False
++        )
+         def upload_ajax_public():
+             if not self.can_upload:
+                 return self.web.error403()
+@@ -377,6 +394,7 @@ class ReceiveModeRequest(Request):
+ 
+             # No errors yet
+             self.upload_error = False
++            self.file_upload_rejected = False
+ 
+             # Figure out what files should be saved
+             now = datetime.now()
+@@ -523,6 +541,16 @@ class ReceiveModeRequest(Request):
+         writable stream.
+         """
+         if self.upload_request:
++            # Check if file uploads are disabled - reject file streams early
++            if self.web.settings.get("receive", "disable_files"):
++                self.web.common.log(
++                    "ReceiveModeRequest",
++                    "_get_file_stream",
++                    "File upload rejected: disable_files is enabled",
++                )
++                self.file_upload_rejected = True
++                return io.BytesIO()
++
+             self.tell_gui_request_started()
+ 
+             self.filename = secure_filename(filename)
+@@ -586,9 +614,24 @@ class ReceiveModeRequest(Request):
+                 self.web.receive_mode.uploads_in_progress.remove(history_id)
+ 
+             # If no files were written to self.receive_mode_dir, delete it
++            # Also delete if file uploads were rejected due to disable_files setting
+             try:
+-                if len(os.listdir(self.receive_mode_dir)) == 0:
+-                    os.rmdir(self.receive_mode_dir)
++                if (
++                    len(os.listdir(self.receive_mode_dir)) == 0
++                    or self.file_upload_rejected
++                ):
++                    # Only delete if there are no actual files (messages are valid)
++                    files = os.listdir(self.receive_mode_dir)
++                    if len(files) == 0:
++                        os.rmdir(self.receive_mode_dir)
++                    elif (
++                        self.file_upload_rejected
++                        and len(files) == 1
++                        and files[0].endswith("-message.txt")
++                    ):
++                        # File upload was rejected and only a message file exists
++                        os.remove(os.path.join(self.receive_mode_dir, files[0]))
++                        os.rmdir(self.receive_mode_dir)
+             except Exception:
+                 pass
+ 
+diff --git a/desktop/tests/test_gui_receive.py b/desktop/tests/test_gui_receive.py
+index 11f2d60..74fdf33 100644
+--- a/desktop/tests/test_gui_receive.py
++++ b/desktop/tests/test_gui_receive.py
+@@ -262,7 +262,87 @@ class TestReceive(GuiBaseTest):
+         self.run_all_receive_mode_setup_tests(tab)
+         self.upload_file(tab, self.tmpfile_test, "test.txt")
+         url = f"http://127.0.0.1:{tab.app.port}/";
+-        self.hit_405(url, expected_resp="OnionShare: 405 Method Not Allowed", data = {'foo':'bar'}, methods = ["put", "post", "delete", "options"])
++        self.hit_405(
++            url,
++            expected_resp="OnionShare: 405 Method Not Allowed",
++            data={"foo": "bar"},
++            methods=["put", "post", "delete", "options"],
++        )
++
++        self.server_is_stopped(tab)
++        self.web_server_is_stopped(tab)
++        self.server_status_indicator_says_closed(tab)
++        self.close_all_tabs()
++
++    def test_disable_files_prevents_file_upload(self):
++        """
++        Test that disable_files setting prevents file uploads (security fix)
++        When disable_files is True, file uploads should be rejected and no files written to disk.
++        """
++        tab = self.new_receive_tab()
++
++        # Enable disable_files setting
++        tab.get_mode().mode_settings_widget.toggle_advanced_button.click()
++        tab.get_mode().mode_settings_widget.disable_files_checkbox.click()
++
++        self.run_all_common_setup_tests()
++        self.run_all_receive_mode_setup_tests(tab)
++
++        # Try to upload a file - should fail
++        files = {"file[]": open(self.tmpfile_test, "rb")}
++        url = f"http://127.0.0.1:{tab.app.port}/upload-ajax";
++        r = requests.post(url, files=files)
++
++        # Verify error response
++        self.assertIn("error_flashes", r.text)
++        self.assertIn("File uploads are not allowed", r.text)
++
++        # Verify no files were written to disk
++        QtTest.QTest.qWait(1000, self.gui.qtapp)
++        written_files = []
++        now = datetime.now()
++        for _ in range(10):
++            date_dir = now.strftime("%Y-%m-%d")
++            time_dir = now.strftime("%H%M%S")
++            receive_mode_dir = os.path.join(
++                tab.settings.get("receive", "data_dir"), date_dir, time_dir
++            )
++            for path in glob.glob(receive_mode_dir + "*"):
++                for root, dirs, files in os.walk(path):
++                    for f in files:
++                        if not f.endswith("-message.txt"):
++                            written_files.append(os.path.join(root, f))
++            now = now - timedelta(seconds=1)
++
++        self.assertEqual(
++            len(written_files),
++            0,
++            f"No files should be written when disable_files is True, but found: {written_files}",
++        )
++
++        self.server_is_stopped(tab)
++        self.web_server_is_stopped(tab)
++        self.server_status_indicator_says_closed(tab)
++        self.close_all_tabs()
++
++    def test_disable_files_allows_text_message(self):
++        """
++        Test that disable_files setting still allows text messages
++        """
++        tab = self.new_receive_tab()
++
++        # Enable disable_files setting
++        tab.get_mode().mode_settings_widget.toggle_advanced_button.click()
++        tab.get_mode().mode_settings_widget.disable_files_checkbox.click()
++
++        self.run_all_common_setup_tests()
++        self.run_all_receive_mode_setup_tests(tab)
++
++        # Submit a text message - should succeed
++        self.submit_message(tab, "This is a test message")
++
++        # Verify counter incremented (text message was accepted)
++        self.counter_incremented(tab, 1)
+ 
+         self.server_is_stopped(tab)
+         self.web_server_is_stopped(tab)
diff -Nru onionshare-2.6.3/debian/patches/0006-Prevent-empty-folder-from-being-created-when-no-file.patch onionshare-2.6.3/debian/patches/0006-Prevent-empty-folder-from-being-created-when-no-file.patch
--- onionshare-2.6.3/debian/patches/0006-Prevent-empty-folder-from-being-created-when-no-file.patch	1970-01-01 01:00:00.000000000 +0100
+++ onionshare-2.6.3/debian/patches/0006-Prevent-empty-folder-from-being-created-when-no-file.patch	2026-07-11 13:30:40.000000000 +0200
@@ -0,0 +1,237 @@
+From: Miguel Jacq <[email protected]>
+Date: Sat, 30 May 2026 18:15:06 +1000
+Subject: Prevent empty folder from being created when no file is uploaded
+
+Origin: upstream, commit:e4ed559908b55cf41b993ed3d02052911025785b
+Bug: https://github.com/onionshare/onionshare/security/advisories/GHSA-v833-3823-cmhp
+Bug-Debian: https://bugs.debian.org/1139716
+---
+ cli/onionshare_cli/web/receive_mode.py | 173 +++++++++++++++++----------------
+ desktop/tests/test_gui_receive.py      |   4 +-
+ 2 files changed, 93 insertions(+), 84 deletions(-)
+
+diff --git a/cli/onionshare_cli/web/receive_mode.py b/cli/onionshare_cli/web/receive_mode.py
+index a5a208a..c99eed4 100644
+--- a/cli/onionshare_cli/web/receive_mode.py
++++ b/cli/onionshare_cli/web/receive_mode.py
+@@ -396,63 +396,9 @@ class ReceiveModeRequest(Request):
+             self.upload_error = False
+             self.file_upload_rejected = False
+ 
+-            # Figure out what files should be saved
+-            now = datetime.now()
+-            date_dir = now.strftime("%Y-%m-%d")
+-            time_dir = now.strftime("%H%M%S%f")
+-            self.receive_mode_dir = os.path.join(
+-                self.web.settings.get("receive", "data_dir"), date_dir, time_dir
+-            )
+-
+-            # Create that directory, which shouldn't exist yet
+-            try:
+-                os.makedirs(self.receive_mode_dir, 0o700, exist_ok=False)
+-            except OSError:
+-                # If this directory already exists, maybe someone else is uploading files at
+-                # the same second, so use a different name in that case
+-                if os.path.exists(self.receive_mode_dir):
+-                    # Keep going until we find a directory name that's available
+-                    i = 1
+-                    while True:
+-                        new_receive_mode_dir = f"{self.receive_mode_dir}-{i}"
+-                        try:
+-                            os.makedirs(new_receive_mode_dir, 0o700, exist_ok=False)
+-                            self.receive_mode_dir = new_receive_mode_dir
+-                            break
+-                        except OSError:
+-                            pass
+-                        i += 1
+-                        # Failsafe
+-                        if i == 100:
+-                            self.web.common.log(
+-                                "ReceiveModeRequest",
+-                                "__init__",
+-                                "Error finding available receive mode directory",
+-                            )
+-                            self.upload_error = True
+-                            break
+-            except PermissionError:
+-                self.web.add_request(
+-                    self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
+-                    request.path,
+-                    {"receive_mode_dir": self.receive_mode_dir},
+-                )
+-                print(
+-                    f"Could not create OnionShare data folder: {self.receive_mode_dir}"
+-                )
+-                self.web.common.log(
+-                    "ReceiveModeRequest",
+-                    "__init__",
+-                    "Permission denied creating receive mode directory",
+-                )
+-                self.upload_error = True
+-
+-            # Figure out the message filename, in case there is a message
+-            self.message_filename = f"{self.receive_mode_dir}-message.txt"
+-
+-            # If there's an error so far, finish early
+-            if self.upload_error:
+-                return
++            # Don't create directory yet - wait to see if there's actual content
++            self.receive_mode_dir = None
++            self.message_filename = None
+ 
+             # A dictionary that maps filenames to the bytes uploaded so far
+             self.progress = {}
+@@ -486,36 +432,95 @@ class ReceiveModeRequest(Request):
+                     if text_message and len(text_message) <= 524288:
+                         if text_message.strip() != "":
+                             self.includes_message = True
++                            # Create directory only if there's a message
++                            self._create_receive_directory()
++                            if not self.upload_error:
++                                self.message_filename = f"{self.receive_mode_dir}-message.txt"
++                                with open(self.message_filename, "w") as f:
++                                    f.write(text_message)
++
++                                self.web.common.log(
++                                    "ReceiveModeRequest",
++                                    "__init__",
++                                    f"saved message to {self.message_filename}",
++                                )
++                                print(f"Received: {self.message_filename}")
++
++                                # Tell the GUI about the message
++                                self.tell_gui_request_started()
++                                self.web.common.log(
++                                    "ReceiveModeRequest",
++                                    "__init__",
++                                    "sending REQUEST_UPLOAD_INCLUDES_MESSAGE to GUI",
++                                )
++                                self.web.add_request(
++                                    self.web.REQUEST_UPLOAD_INCLUDES_MESSAGE,
++                                    self.path,
++                                    {
++                                        "id": self.history_id,
++                                        "filename": self.message_filename,
++                                    },
++                                )
++
++    def _create_receive_directory(self):
++        """
++        Create the receive mode directory. Called only when there's actual content to save.
++        """
++        if self.receive_mode_dir is not None:
++            return  # Already created
++
++        now = datetime.now()
++        date_dir = now.strftime("%Y-%m-%d")
++        time_dir = now.strftime("%H%M%S%f")
++        self.receive_mode_dir = os.path.join(
++            self.web.settings.get("receive", "data_dir"), date_dir, time_dir
++        )
+ 
+-                            with open(self.message_filename, "w") as f:
+-                                f.write(text_message)
+-
+-                            self.web.common.log(
+-                                "ReceiveModeRequest",
+-                                "__init__",
+-                                f"saved message to {self.message_filename}",
+-                            )
+-                            print(f"Received: {self.message_filename}")
+-
+-                            # Tell the GUI about the message
+-                            self.tell_gui_request_started()
+-                            self.web.common.log(
+-                                "ReceiveModeRequest",
+-                                "__init__",
+-                                "sending REQUEST_UPLOAD_INCLUDES_MESSAGE to GUI",
+-                            )
+-                            self.web.add_request(
+-                                self.web.REQUEST_UPLOAD_INCLUDES_MESSAGE,
+-                                self.path,
+-                                {
+-                                    "id": self.history_id,
+-                                    "filename": self.message_filename,
+-                                },
+-                            )
++        try:
++            os.makedirs(self.receive_mode_dir, 0o700, exist_ok=False)
++        except OSError:
++            if os.path.exists(self.receive_mode_dir):
++                i = 1
++                while True:
++                    new_receive_mode_dir = f"{self.receive_mode_dir}-{i}"
++                    try:
++                        os.makedirs(new_receive_mode_dir, 0o700, exist_ok=False)
++                        self.receive_mode_dir = new_receive_mode_dir
++                        break
++                    except OSError:
++                        pass
++                    i += 1
++                    if i == 100:
++                        self.web.common.log(
++                            "ReceiveModeRequest",
++                            "_create_receive_directory",
++                            "Error finding available receive mode directory",
++                        )
++                        self.upload_error = True
++                        break
++        except PermissionError:
++            self.web.add_request(
++                self.web.REQUEST_ERROR_DATA_DIR_CANNOT_CREATE,
++                request.path,
++                {"receive_mode_dir": self.receive_mode_dir},
++            )
++            print(
++                f"Could not create OnionShare data folder: {self.receive_mode_dir}"
++            )
++            self.web.common.log(
++                "ReceiveModeRequest",
++                "_create_receive_directory",
++                "Permission denied creating receive mode directory",
++            )
++            self.upload_error = True
+ 
+     def tell_gui_request_started(self):
+         # Tell the GUI about the request
+         if not self.told_gui_about_request:
++            # Create directory if not already created (for file uploads)
++            if self.receive_mode_dir is None and not self.upload_error:
++                self._create_receive_directory()
++
+             self.web.common.log(
+                 "ReceiveModeRequest",
+                 "tell_gui_request_started",
+@@ -540,6 +545,10 @@ class ReceiveModeRequest(Request):
+         This gets called for each file that gets uploaded, and returns an file-like
+         writable stream.
+         """
++        # Ignore empty file uploads (user didn't select a file)
++        if not filename or filename.strip() == "":
++            return io.BytesIO()
++
+         if self.upload_request:
+             # Check if file uploads are disabled - reject file streams early
+             if self.web.settings.get("receive", "disable_files"):
+diff --git a/desktop/tests/test_gui_receive.py b/desktop/tests/test_gui_receive.py
+index 74fdf33..10dd555 100644
+--- a/desktop/tests/test_gui_receive.py
++++ b/desktop/tests/test_gui_receive.py
+@@ -283,7 +283,7 @@ class TestReceive(GuiBaseTest):
+ 
+         # Enable disable_files setting
+         tab.get_mode().mode_settings_widget.toggle_advanced_button.click()
+-        tab.get_mode().mode_settings_widget.disable_files_checkbox.click()
++        tab.get_mode().disable_files_checkbox.click()
+ 
+         self.run_all_common_setup_tests()
+         self.run_all_receive_mode_setup_tests(tab)
+@@ -333,7 +333,7 @@ class TestReceive(GuiBaseTest):
+ 
+         # Enable disable_files setting
+         tab.get_mode().mode_settings_widget.toggle_advanced_button.click()
+-        tab.get_mode().mode_settings_widget.disable_files_checkbox.click()
++        tab.get_mode().disable_files_checkbox.click()
+ 
+         self.run_all_common_setup_tests()
+         self.run_all_receive_mode_setup_tests(tab)
diff -Nru onionshare-2.6.3/debian/patches/0007-Updates-test-to-check-no-receive-directory-created-i.patch onionshare-2.6.3/debian/patches/0007-Updates-test-to-check-no-receive-directory-created-i.patch
--- onionshare-2.6.3/debian/patches/0007-Updates-test-to-check-no-receive-directory-created-i.patch	1970-01-01 01:00:00.000000000 +0100
+++ onionshare-2.6.3/debian/patches/0007-Updates-test-to-check-no-receive-directory-created-i.patch	2026-07-11 13:30:40.000000000 +0200
@@ -0,0 +1,38 @@
+From: Saptak S <[email protected]>
+Date: Tue, 9 Jun 2026 12:00:09 +0530
+Subject: Updates test to check no receive directory created if no file or
+ message
+
+Origin: upstream, commit:1d76494f5d819adc4c69f08f3842d564a425aa89
+Bug: https://github.com/onionshare/onionshare/security/advisories/GHSA-v833-3823-cmhp
+Bug-Debian: https://bugs.debian.org/1139716
+---
+ cli/tests/test_cli_web.py | 8 ++++----
+ 1 file changed, 4 insertions(+), 4 deletions(-)
+
+diff --git a/cli/tests/test_cli_web.py b/cli/tests/test_cli_web.py
+index fd5ca7b..a2ba99c 100644
+--- a/cli/tests/test_cli_web.py
++++ b/cli/tests/test_cli_web.py
+@@ -257,6 +257,7 @@ class TestWeb:
+ 
+         # Date folder should have just a time folder with new_york.jpg
+         data_dir_date = os.path.join(data_dir, os.listdir(data_dir)[0])
++        print(data_dir_date)
+         filenames = os.listdir(data_dir_date)
+         assert len(filenames) == 1
+         time_str = filenames[0][0:12]
+@@ -287,10 +288,9 @@ class TestWeb:
+             assert res.status_code == 200
+             assert b"Nothing submitted" in content
+ 
+-        # Date folder should be empty
+-        data_dir_date = os.path.join(data_dir, os.listdir(data_dir)[0])
+-        filenames = os.listdir(data_dir_date)
+-        assert len(filenames) == 0
++        # There should be no receive date directory
++        data_dir_date = os.listdir(data_dir)
++        assert len(data_dir_date) == 0
+ 
+         shutil.rmtree(data_dir)
+ 
diff -Nru onionshare-2.6.3/debian/patches/0008-Prevent-symlink-traversal-in-share-website-mode.patch onionshare-2.6.3/debian/patches/0008-Prevent-symlink-traversal-in-share-website-mode.patch
--- onionshare-2.6.3/debian/patches/0008-Prevent-symlink-traversal-in-share-website-mode.patch	1970-01-01 01:00:00.000000000 +0100
+++ onionshare-2.6.3/debian/patches/0008-Prevent-symlink-traversal-in-share-website-mode.patch	2026-07-11 13:30:40.000000000 +0200
@@ -0,0 +1,515 @@
+From: Miguel Jacq <[email protected]>
+Date: Sat, 30 May 2026 16:48:51 +1000
+Subject: Prevent symlink traversal in share/website mode
+
+Origin: upstream, commit:48f31cfac077fcc9c04c67c2a6dbf87d956f5eec
+Bug: https://github.com/onionshare/onionshare/security/advisories/GHSA-22p9-r2f5-22mf
+Bug-Debian: https://bugs.debian.org/1139717
+---
+ cli/onionshare_cli/web/send_base_mode.py |  73 +++++++--
+ cli/onionshare_cli/web/share_mode.py     |  35 ++--
+ cli/tests/test_cli_web_symlink.py        | 267 +++++++++++++++++++++++++++++++
+ 3 files changed, 356 insertions(+), 19 deletions(-)
+ create mode 100644 cli/tests/test_cli_web_symlink.py
+
+diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py
+index e55841a..b9cff9a 100644
+--- a/cli/onionshare_cli/web/send_base_mode.py
++++ b/cli/onionshare_cli/web/send_base_mode.py
+@@ -74,6 +74,12 @@ class SendBaseModeWeb:
+         """
+         Build a data structure that describes the list of files
+         """
++        # Store canonical paths of selected root directories for containment checking
++        self.selected_roots = []
++        for filename in filenames:
++            if os.path.isdir(filename):
++                self.selected_roots.append(os.path.realpath(filename))
++
+         # If there's just one folder, replace filenames with a list of files inside that folder
+         if len(filenames) == 1 and os.path.isdir(filenames[0]):
+             filenames = [
+@@ -101,8 +107,15 @@ class SendBaseModeWeb:
+         for filename in filenames:
+             basename = os.path.basename(filename.rstrip(slash))
+ 
++            # Skip symlinks at the root level
++            if os.path.islink(filename):
++                continue
++
+             # If it's a filename, add it
+             if os.path.isfile(filename):
++                # Verify the file is within selected roots (for symlink safety)
++                if not self._is_path_contained(filename):
++                    continue
+                 self.files[self.fix_windows_paths(basename)] = filename
+                 self.root_files[self.fix_windows_paths(basename)] = filename
+ 
+@@ -110,7 +123,7 @@ class SendBaseModeWeb:
+             elif os.path.isdir(filename):
+                 self.root_files[self.fix_windows_paths(basename)] = filename
+ 
+-                for root, _, nested_filenames in os.walk(filename):
++                for root, _, nested_filenames in os.walk(filename, followlinks=False):
+                     # Normalize the root path. So if the directory name is "/home/user/Documents/some_folder",
+                     # and it has a nested folder foobar, the root is "/home/user/Documents/some_folder/foobar".
+                     # The normalized_root should be "some_folder/foobar"
+@@ -123,15 +136,39 @@ class SendBaseModeWeb:
+ 
+                     # Add the files in this dir
+                     for nested_filename in nested_filenames:
++                        full_path = os.path.join(root, nested_filename)
++                        # Skip symlinks
++                        if os.path.islink(full_path):
++                            continue
++                        # Verify the file is within selected roots
++                        if not self._is_path_contained(full_path):
++                            continue
+                         self.files[
+                             self.fix_windows_paths(
+                                 os.path.join(normalized_root, nested_filename)
+                             )
+-                        ] = os.path.join(root, nested_filename)
++                        ] = full_path
+ 
+         self.set_file_info_custom(filenames, processed_size_callback)
+ 
+-    def directory_listing(self, filenames, path="", filesystem_path=None, add_trailing_slash=False):
++    def _is_path_contained(self, path):
++        """
++        Check if the given path is contained within one of the selected root directories.
++        Uses realpath to resolve symlinks and ensure containment.
++        Returns False if the path is outside the selected roots (potential symlink escape).
++        """
++        if not self.selected_roots:
++            return True
++
++        resolved_path = os.path.realpath(path)
++        for root in self.selected_roots:
++            if resolved_path.startswith(root + os.sep) or resolved_path == root:
++                return True
++        return False
++
++    def directory_listing(
++        self, filenames, path="", filesystem_path=None, add_trailing_slash=False
++    ):
+         """
+         Display the front page of a share or index.html-less website, listing the files/directories.
+         """
+@@ -153,7 +190,9 @@ class SendBaseModeWeb:
+         breadcrumbs_leaf = breadcrumbs.pop()[0]
+ 
+         # If filesystem_path is None, this is the root directory listing
+-        files, dirs = self.build_directory_listing(path, filenames, filesystem_path, add_trailing_slash)
++        files, dirs = self.build_directory_listing(
++            path, filenames, filesystem_path, add_trailing_slash
++        )
+ 
+         # Mark the request as done so we know we can close the share if in auto-stop mode.
+         self.web.done = True
+@@ -163,7 +202,9 @@ class SendBaseModeWeb:
+             path, files, dirs, breadcrumbs, breadcrumbs_leaf
+         )
+ 
+-    def build_directory_listing(self, path, filenames, filesystem_path, add_trailing_slash=False):
++    def build_directory_listing(
++        self, path, filenames, filesystem_path, add_trailing_slash=False
++    ):
+         files = []
+         dirs = []
+ 
+@@ -178,11 +219,17 @@ class SendBaseModeWeb:
+             if is_dir:
+                 if add_trailing_slash:
+                     dirs.append(
+-                        {"link": os.path.join(f"/{path}", quote(filename), ""), "basename": filename}
++                        {
++                            "link": os.path.join(f"/{path}", quote(filename), ""),
++                            "basename": filename,
++                        }
+                     )
+                 else:
+                     dirs.append(
+-                        {"link": os.path.join(f"/{path}", quote(filename)), "basename": filename}
++                        {
++                            "link": os.path.join(f"/{path}", quote(filename)),
++                            "basename": filename,
++                        }
+                     )
+             else:
+                 size = os.path.getsize(this_filesystem_path)
+@@ -202,6 +249,12 @@ class SendBaseModeWeb:
+         Return a flask response that's streaming the download of an individual file, and gzip
+         compressing it if the browser supports it.
+         """
++        # Verify the path is contained within selected roots (symlink safety check)
++        if not self._is_path_contained(filesystem_path):
++            history_id = self.cur_history_id
++            self.cur_history_id += 1
++            return self.web.error404(history_id)
++
+         use_gzip = self.should_use_gzip()
+ 
+         # gzip compress the individual file, if it hasn't already been compressed
+@@ -256,11 +309,13 @@ class SendBaseModeWeb:
+                             if self.web.settings.get(self.web.mode, "log_filenames"):
+                                 # Decode and sanitize the path to remove newlines
+                                 decoded_path = unquote(path)
+-                                decoded_path = decoded_path.replace("\r", "").replace("\n", "")
++                                decoded_path = decoded_path.replace("\r", "").replace(
++                                    "\n", ""
++                                )
+                                 filename_str = f"{decoded_path} - "
+                             else:
+                                 filename_str = ""
+-                            
++
+                             sys.stdout.write(
+                                 "\r{0}{1:s}, {2:.2f}%          ".format(
+                                     filename_str,
+diff --git a/cli/onionshare_cli/web/share_mode.py b/cli/onionshare_cli/web/share_mode.py
+index 49d8728..1a86c37 100644
+--- a/cli/onionshare_cli/web/share_mode.py
++++ b/cli/onionshare_cli/web/share_mode.py
+@@ -300,9 +300,7 @@ class ShareModeWeb(SendBaseModeWeb):
+ 
+         return range_, status_code
+ 
+-    def generate(
+-        self, range_, file_to_download, path, history_id, filesize
+-    ):
++    def generate(self, range_, file_to_download, path, history_id, filesize):
+         # The user hasn't canceled the download
+         self.client_cancel = False
+ 
+@@ -349,7 +347,9 @@ class ShareModeWeb(SendBaseModeWeb):
+                         if self.web.settings.get("share", "log_filenames"):
+                             # Decode and sanitize the path to remove newlines
+                             decoded_path = unquote(path)
+-                            decoded_path = decoded_path.replace("\r", "").replace("\n", "")
++                            decoded_path = decoded_path.replace("\r", "").replace(
++                                "\n", ""
++                            )
+                             filename_str = f"{decoded_path} - "
+                         else:
+                             filename_str = ""
+@@ -576,6 +576,14 @@ class ZipWriter(object):
+         """
+         Add a file to the zip archive.
+         """
++        # Skip symlinks
++        if os.path.islink(filename):
++            return
++        # Verify the file is within selected roots (symlink safety check)
++        # Only check if web/share_mode is available (may be None in standalone usage)
++        if self.web and hasattr(self.web, "share_mode") and self.web.share_mode:
++            if not self.web.share_mode._is_path_contained(filename):
++                return
+         self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
+         self._size += os.path.getsize(filename)
+         self.processed_size_callback(self._size)
+@@ -585,18 +593,25 @@ class ZipWriter(object):
+         Add a directory, and all of its children, to the zip archive.
+         """
+         dir_to_strip = os.path.dirname(filename.rstrip("/")) + "/"
+-        for dirpath, dirnames, filenames in os.walk(filename):
++        for dirpath, dirnames, filenames in os.walk(filename, followlinks=False):
+             for f in filenames:
+                 # Canceling early?
+                 if self.cancel_compression:
+                     return False
+ 
+                 full_filename = os.path.join(dirpath, f)
+-                if not os.path.islink(full_filename):
+-                    arc_filename = full_filename[len(dir_to_strip) :]
+-                    self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
+-                    self._size += os.path.getsize(full_filename)
+-                    self.processed_size_callback(self._size)
++                # Skip symlinks
++                if os.path.islink(full_filename):
++                    continue
++                # Verify the file is within selected roots
++                # Only check if web/share_mode is available (may be None in standalone usage)
++                if self.web and hasattr(self.web, "share_mode") and self.web.share_mode:
++                    if not self.web.share_mode._is_path_contained(full_filename):
++                        continue
++                arc_filename = full_filename[len(dir_to_strip) :]
++                self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
++                self._size += os.path.getsize(full_filename)
++                self.processed_size_callback(self._size)
+ 
+         return True
+ 
+diff --git a/cli/tests/test_cli_web_symlink.py b/cli/tests/test_cli_web_symlink.py
+new file mode 100644
+index 0000000..526199b
+--- /dev/null
++++ b/cli/tests/test_cli_web_symlink.py
+@@ -0,0 +1,267 @@
++import os
++import shutil
++import tempfile
++
++import pytest
++
++from onionshare_cli.common import Common
++from onionshare_cli.web import Web
++from onionshare_cli.settings import Settings
++from onionshare_cli.mode_settings import ModeSettings
++
++
++class TestSymlinkProtection:
++    """Tests for symlink vulnerabilities"""
++
++    @pytest.fixture
++    def symlink_test_setup(self, temp_dir):
++        """
++        Create a test environment with:
++        - A shared directory containing normal files and symlinks
++        - A secret file outside the shared directory
++        - Symlinks pointing to the secret file
++        """
++        # Create a unique temp directory for this test setup
++        test_temp_dir = tempfile.TemporaryDirectory(dir=temp_dir.name)
++
++        # Create the secret file outside the shared directory
++        outside_secret = os.path.join(test_temp_dir.name, "outside-secret.txt")
++        with open(outside_secret, "w") as f:
++            f.write("OUTSIDE_SECRET_MARKER")
++
++        # Create the shared directory
++        site_dir = os.path.join(test_temp_dir.name, "site")
++        os.mkdir(site_dir)
++
++        # Create a normal file in the shared directory
++        normal_file = os.path.join(site_dir, "normal.txt")
++        with open(normal_file, "w") as f:
++            f.write("NORMAL_FILE_CONTENT")
++
++        # Create a symlink to the secret inside the shared directory
++        symlink_path = os.path.join(site_dir, "link.txt")
++        os.symlink(outside_secret, symlink_path)
++
++        # Create a nested directory with files and symlinks
++        nested_dir = os.path.join(site_dir, "nested")
++        os.mkdir(nested_dir)
++
++        nested_file = os.path.join(nested_dir, "nested.txt")
++        with open(nested_file, "w") as f:
++            f.write("NESTED_FILE_CONTENT")
++
++        nested_symlink = os.path.join(nested_dir, "nested-link.txt")
++        os.symlink(outside_secret, nested_symlink)
++
++        return {
++            "temp_dir": test_temp_dir,
++            "outside_secret": outside_secret,
++            "site_dir": site_dir,
++            "normal_file": normal_file,
++            "symlink_path": symlink_path,
++            "nested_dir": nested_dir,
++            "nested_file": nested_file,
++            "nested_symlink": nested_symlink,
++        }
++
++    def test_symlink_excluded_from_file_inventory_website_mode(
++        self, symlink_test_setup, common_obj
++    ):
++        """Test that symlinks are excluded from file inventory in website mode"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        # Symlinks should not be in the files dictionary
++        assert "link.txt" not in web.website_mode.files
++        assert "nested/nested-link.txt" not in web.website_mode.files
++
++        # Normal files should be in the files dictionary
++        assert "normal.txt" in web.website_mode.files
++        assert "nested/nested.txt" in web.website_mode.files
++
++    def test_symlink_excluded_from_file_inventory_share_mode(
++        self, symlink_test_setup, common_obj
++    ):
++        """Test that symlinks are excluded from file inventory in share mode"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "share")
++        web.share_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        # Symlinks should not be in the files dictionary
++        assert "link.txt" not in web.share_mode.files
++        assert "nested/nested-link.txt" not in web.share_mode.files
++
++        # Normal files should be in the files dictionary
++        assert "normal.txt" in web.share_mode.files
++        assert "nested/nested.txt" in web.share_mode.files
++
++    def test_is_path_contained_normal_files(self, symlink_test_setup, common_obj):
++        """Test that normal files are correctly identified as contained"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        # Normal files should be contained
++        assert web.website_mode._is_path_contained(symlink_test_setup["normal_file"])
++        assert web.website_mode._is_path_contained(symlink_test_setup["nested_file"])
++
++    def test_is_path_contained_symlinks(self, symlink_test_setup, common_obj):
++        """Test that symlinks to outside are correctly identified as NOT contained"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        # Symlinks to outside should NOT be contained
++        assert not web.website_mode._is_path_contained(symlink_test_setup["symlink_path"])
++        assert not web.website_mode._is_path_contained(symlink_test_setup["nested_symlink"])
++
++    def test_symlink_rejected_in_website_mode_request(
++        self, symlink_test_setup, common_obj
++    ):
++        """Test that requesting a symlink returns 404 in website mode"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.app.testing = True
++        web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        with web.app.test_client() as c:
++            # Requesting symlink should return 404
++            res = c.get("/link.txt")
++            assert res.status_code == 404
++
++            # Normal file should return 200
++            res = c.get("/normal.txt")
++            assert res.status_code == 200
++            assert b"NORMAL_FILE_CONTENT" in res.get_data()
++
++    def test_symlink_rejected_in_share_mode_individual_download(
++        self, symlink_test_setup, common_obj
++    ):
++        """Test that individual file downloads reject symlinks in share mode"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "share")
++        web.app.testing = True
++        web.settings.set("share", "autostop_sharing", False)
++        web.share_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        with web.app.test_client() as c:
++            # Requesting symlink should return 404
++            res = c.get("/link.txt")
++            assert res.status_code == 404
++
++            # Normal file should return 200
++            res = c.get("/normal.txt")
++            assert res.status_code == 200
++            assert b"NORMAL_FILE_CONTENT" in res.get_data()
++
++    def test_symlink_excluded_from_zip_archive(self, symlink_test_setup, common_obj):
++        """Test that symlinks are excluded from ZIP archives"""
++        import zipfile
++        from io import BytesIO
++
++        web = Web(common_obj, False, ModeSettings(common_obj), "share")
++        web.app.testing = True
++        web.share_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        with web.app.test_client() as c:
++            res = c.get("/download")
++            assert res.status_code == 200
++
++            # Check ZIP contents
++            zip_data = BytesIO(res.get_data())
++            with zipfile.ZipFile(zip_data, "r") as zf:
++                zip_contents = zf.namelist()
++
++                # Symlinks should not be in the ZIP
++                assert "link.txt" not in zip_contents
++                assert "nested/nested-link.txt" not in zip_contents
++
++                # Normal files should be in the ZIP
++                assert "normal.txt" in zip_contents
++                assert "nested/nested.txt" in zip_contents
++
++    def test_root_level_symlink_handling(self, temp_dir, common_obj):
++        """Test that root-level symlinks (not in subdirectory) are handled correctly"""
++        # Create a directory with only a symlink (no normal files)
++        site_dir = os.path.join(temp_dir.name, "site-only-symlink")
++        os.mkdir(site_dir)
++
++        outside_secret = os.path.join(temp_dir.name, "secret.txt")
++        with open(outside_secret, "w") as f:
++            f.write("SECRET")
++
++        symlink_path = os.path.join(site_dir, "link.txt")
++        os.symlink(outside_secret, symlink_path)
++
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.website_mode.set_file_info([site_dir])
++
++        # The symlink should not be in the file inventory
++        assert "link.txt" not in web.website_mode.files
++
++    def test_nested_symlink_chain_handling(self, temp_dir, common_obj):
++        """Test that nested symlinks (symlinks within subdirectories) are handled"""
++        site_dir = os.path.join(temp_dir.name, "site")
++        os.mkdir(site_dir)
++
++        outside_secret = os.path.join(temp_dir.name, "secret.txt")
++        with open(outside_secret, "w") as f:
++            f.write("SECRET")
++
++        # Create nested directory structure with symlinks at various levels
++        level1 = os.path.join(site_dir, "level1")
++        os.mkdir(level1)
++
++        level2 = os.path.join(level1, "level2")
++        os.mkdir(level2)
++
++        # Symlink in level1
++        symlink1 = os.path.join(level1, "link1.txt")
++        os.symlink(outside_secret, symlink1)
++
++        # Symlink in level2
++        symlink2 = os.path.join(level2, "link2.txt")
++        os.symlink(outside_secret, symlink2)
++
++        # Normal file in level2
++        normal_file = os.path.join(level2, "normal.txt")
++        with open(normal_file, "w") as f:
++            f.write("NORMAL")
++
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.website_mode.set_file_info([site_dir])
++
++        # Symlinks should not be in the file inventory
++        assert "level1/link1.txt" not in web.website_mode.files
++        assert "level1/level2/link2.txt" not in web.website_mode.files
++
++        # Normal file should be in the file inventory
++        assert "level1/level2/normal.txt" in web.website_mode.files
++
++    def test_directory_symlink_handling(self, temp_dir, common_obj):
++        """Test that symlinks to directories are handled correctly"""
++        # Create a unique temp directory for this test
++        test_temp_dir = tempfile.TemporaryDirectory(dir=temp_dir.name)
++
++        site_dir = os.path.join(test_temp_dir.name, "site")
++        os.mkdir(site_dir)
++
++        outside_dir = os.path.join(temp_dir.name, "outside-dir")
++        os.mkdir(outside_dir)
++
++        outside_file = os.path.join(outside_dir, "outside-file.txt")
++        with open(outside_file, "w") as f:
++            f.write("OUTSIDE")
++
++        # Create a symlink to a directory
++        dir_symlink = os.path.join(site_dir, "link-dir")
++        os.symlink(outside_dir, dir_symlink)
++
++        # Create a normal file
++        normal_file = os.path.join(site_dir, "normal.txt")
++        with open(normal_file, "w") as f:
++            f.write("NORMAL")
++
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.website_mode.set_file_info([site_dir])
++
++        # The directory symlink should not be in root_files
++        assert "link-dir" not in web.website_mode.root_files
++
++        # Normal file should be in root_files
++        assert "normal.txt" in web.website_mode.root_files
diff -Nru onionshare-2.6.3/debian/patches/0009-Do-not-show-symlinks-that-go-outside-the-docroot-in-.patch onionshare-2.6.3/debian/patches/0009-Do-not-show-symlinks-that-go-outside-the-docroot-in-.patch
--- onionshare-2.6.3/debian/patches/0009-Do-not-show-symlinks-that-go-outside-the-docroot-in-.patch	1970-01-01 01:00:00.000000000 +0100
+++ onionshare-2.6.3/debian/patches/0009-Do-not-show-symlinks-that-go-outside-the-docroot-in-.patch	2026-07-11 13:30:40.000000000 +0200
@@ -0,0 +1,122 @@
+From: Miguel Jacq <[email protected]>
+Date: Sat, 30 May 2026 18:25:49 +1000
+Subject: Do not show symlinks that go outside the docroot in nested dirs
+ either
+
+Origin: upstream, commit:96827bdd0580bd34b921a7b269198f65434178d8
+Bug: https://github.com/onionshare/onionshare/security/advisories/GHSA-22p9-r2f5-22mf
+Bug-Debian: https://bugs.debian.org/1139717
+---
+ cli/onionshare_cli/web/send_base_mode.py |  4 +++
+ cli/onionshare_cli/web/share_mode.py     | 12 +++-----
+ cli/tests/test_cli_web_symlink.py        | 48 ++++++++++++++++++++++++++++++--
+ 3 files changed, 54 insertions(+), 10 deletions(-)
+
+diff --git a/cli/onionshare_cli/web/send_base_mode.py b/cli/onionshare_cli/web/send_base_mode.py
+index b9cff9a..68267ce 100644
+--- a/cli/onionshare_cli/web/send_base_mode.py
++++ b/cli/onionshare_cli/web/send_base_mode.py
+@@ -214,6 +214,10 @@ class SendBaseModeWeb:
+             else:
+                 this_filesystem_path = self.files[filename]
+ 
++            # Skip symlinks in directory listings
++            if os.path.islink(this_filesystem_path):
++                continue
++
+             is_dir = os.path.isdir(this_filesystem_path)
+ 
+             if is_dir:
+diff --git a/cli/onionshare_cli/web/share_mode.py b/cli/onionshare_cli/web/share_mode.py
+index 1a86c37..d6760de 100644
+--- a/cli/onionshare_cli/web/share_mode.py
++++ b/cli/onionshare_cli/web/share_mode.py
+@@ -580,10 +580,8 @@ class ZipWriter(object):
+         if os.path.islink(filename):
+             return
+         # Verify the file is within selected roots (symlink safety check)
+-        # Only check if web/share_mode is available (may be None in standalone usage)
+-        if self.web and hasattr(self.web, "share_mode") and self.web.share_mode:
+-            if not self.web.share_mode._is_path_contained(filename):
+-                return
++        if not self.web.share_mode._is_path_contained(filename):
++            return
+         self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
+         self._size += os.path.getsize(filename)
+         self.processed_size_callback(self._size)
+@@ -604,10 +602,8 @@ class ZipWriter(object):
+                 if os.path.islink(full_filename):
+                     continue
+                 # Verify the file is within selected roots
+-                # Only check if web/share_mode is available (may be None in standalone usage)
+-                if self.web and hasattr(self.web, "share_mode") and self.web.share_mode:
+-                    if not self.web.share_mode._is_path_contained(full_filename):
+-                        continue
++                if not self.web.share_mode._is_path_contained(full_filename):
++                    continue
+                 arc_filename = full_filename[len(dir_to_strip) :]
+                 self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
+                 self._size += os.path.getsize(full_filename)
+diff --git a/cli/tests/test_cli_web_symlink.py b/cli/tests/test_cli_web_symlink.py
+index 526199b..86fb4bf 100644
+--- a/cli/tests/test_cli_web_symlink.py
++++ b/cli/tests/test_cli_web_symlink.py
+@@ -109,8 +109,12 @@ class TestSymlinkProtection:
+         web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
+ 
+         # Symlinks to outside should NOT be contained
+-        assert not web.website_mode._is_path_contained(symlink_test_setup["symlink_path"])
+-        assert not web.website_mode._is_path_contained(symlink_test_setup["nested_symlink"])
++        assert not web.website_mode._is_path_contained(
++            symlink_test_setup["symlink_path"]
++        )
++        assert not web.website_mode._is_path_contained(
++            symlink_test_setup["nested_symlink"]
++        )
+ 
+     def test_symlink_rejected_in_website_mode_request(
+         self, symlink_test_setup, common_obj
+@@ -265,3 +269,43 @@ class TestSymlinkProtection:
+ 
+         # Normal file should be in root_files
+         assert "normal.txt" in web.website_mode.root_files
++
++    def test_symlink_filtered_from_directory_listing(
++        self, symlink_test_setup, common_obj
++    ):
++        """Test that symlinks are filtered from directory listings (not just file inventory)"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.app.testing = True
++        web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        with web.app.test_client() as c:
++            # Request root directory listing
++            res = c.get("/")
++            assert res.status_code == 200
++            data = res.get_data(as_text=True)
++
++            # Symlinks should not appear in directory listing
++            assert "link.txt" not in data or "nested-link.txt" not in data
++
++            # Normal files should appear in directory listing
++            assert "normal.txt" in data
++
++    def test_nested_symlink_filtered_from_directory_listing(
++        self, symlink_test_setup, common_obj
++    ):
++        """Test that nested symlinks are filtered from nested directory listings"""
++        web = Web(common_obj, False, ModeSettings(common_obj), "website")
++        web.app.testing = True
++        web.website_mode.set_file_info([symlink_test_setup["site_dir"]])
++
++        with web.app.test_client() as c:
++            # Request nested directory listing
++            res = c.get("/nested/")
++            assert res.status_code == 200
++            data = res.get_data(as_text=True)
++
++            # Nested symlink should not appear in directory listing
++            assert "nested-link.txt" not in data
++
++            # Normal nested file should appear in directory listing
++            assert "nested.txt" in data
diff -Nru onionshare-2.6.3/debian/patches/0010-Adds-additional-cheks-for-failing-condition.patch onionshare-2.6.3/debian/patches/0010-Adds-additional-cheks-for-failing-condition.patch
--- onionshare-2.6.3/debian/patches/0010-Adds-additional-cheks-for-failing-condition.patch	1970-01-01 01:00:00.000000000 +0100
+++ onionshare-2.6.3/debian/patches/0010-Adds-additional-cheks-for-failing-condition.patch	2026-07-11 13:30:40.000000000 +0200
@@ -0,0 +1,33 @@
+From: Saptak S <[email protected]>
+Date: Tue, 9 Jun 2026 12:06:01 +0530
+Subject: Adds additional cheks for failing condition
+
+Origin: upstream, commit:d0697fa3d05a193138c6052422c313612301d98a
+Bug: https://github.com/onionshare/onionshare/security/advisories/GHSA-22p9-r2f5-22mf
+Bug-Debian: https://bugs.debian.org/1139717
+---
+ cli/onionshare_cli/web/share_mode.py | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/cli/onionshare_cli/web/share_mode.py b/cli/onionshare_cli/web/share_mode.py
+index d6760de..bdf6087 100644
+--- a/cli/onionshare_cli/web/share_mode.py
++++ b/cli/onionshare_cli/web/share_mode.py
+@@ -580,7 +580,7 @@ class ZipWriter(object):
+         if os.path.islink(filename):
+             return
+         # Verify the file is within selected roots (symlink safety check)
+-        if not self.web.share_mode._is_path_contained(filename):
++        if self.web and not self.web.share_mode._is_path_contained(filename):
+             return
+         self.z.write(filename, os.path.basename(filename), zipfile.ZIP_DEFLATED)
+         self._size += os.path.getsize(filename)
+@@ -602,7 +602,7 @@ class ZipWriter(object):
+                 if os.path.islink(full_filename):
+                     continue
+                 # Verify the file is within selected roots
+-                if not self.web.share_mode._is_path_contained(full_filename):
++                if self.web and not self.web.share_mode._is_path_contained(full_filename):
+                     continue
+                 arc_filename = full_filename[len(dir_to_strip) :]
+                 self.z.write(full_filename, arc_filename, zipfile.ZIP_DEFLATED)
diff -Nru onionshare-2.6.3/debian/patches/series onionshare-2.6.3/debian/patches/series
--- onionshare-2.6.3/debian/patches/series	2025-03-05 02:53:15.000000000 +0100
+++ onionshare-2.6.3/debian/patches/series	2026-07-11 13:30:40.000000000 +0200
@@ -2,3 +2,9 @@
 0002-add-pytest.ini-to-make-sure-pytest-can-find-all-test.patch
 0003-Do-not-install-onionshare_cli-script.patch
 0004-Remove-cython-dependency.patch
+0005-Prevent-writing-files-in-Receive-mode-when-file-uplo.patch
+0006-Prevent-empty-folder-from-being-created-when-no-file.patch
+0007-Updates-test-to-check-no-receive-directory-created-i.patch
+0008-Prevent-symlink-traversal-in-share-website-mode.patch
+0009-Do-not-show-symlinks-that-go-outside-the-docroot-in-.patch
+0010-Adds-additional-cheks-for-failing-condition.patch

Attachment: signature.asc
Description: This is a digitally signed message part.

Reply via email to