Bug#1141858: trixie-pu: package onionshare/2.6.3+deb13u1

Hefee <[email protected]>
Newsgroups gmane.linux.debian.devel.release
Message-ID <4698242.Wku2Vz74k6__37127.892176953$1783791463$gmane$org@tuxin>
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
onionshare_2.6.3-1+deb13u2.debdiff (text/x-patch, 57.5 KB)
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
signature.asc (application/pgp-signature, 833 B)
-----BEGIN PGP SIGNATURE-----

iQIzBAABCgAdFiEEOewRoCAWtykmSRoG462wCFBgVjYFAmpSfocACgkQ462wCFBg
VjaiYhAAxAiU+phRQu/+zOIbg0ZIBX1ypIYoShUuz4bI0aRlExbdVJdUY/gDcaBg
60F3cy1Rahpd2P9ERIXHQzMDE2jqXMAPpKGm/9oN61PWxBA8WI0M0DBRzCVEShPF
lz9pvdnhnbTrXVcnJLv7+C9r4ySfXTsiyweP3oRzC0F8Fci110PQ6KXWCmc+Ipvg
ODhDnlPs/IqP6d8hkVCWlgGLWgirjNE47oL1PGIYw2zBMb6uwRha+8ykUBaBWAm6
T/njsQ1R/itDr/6CTEX0flVcILG/9gyDxel5yC2yxl80sX9+6ylUQOHctIxc52jY
X2f7vj4dvKH/hrywn5zp+GhyWQLa1QWTSTC0X3UlJol01ptQ9vcY67aW85cq6h22
m+v3t7E+9IU/ey7IHMXc2Cxfc4avdz46nsew/d7q3uKCjy67+bsUUcect3egXcvK
VeKLV75S4wNXpWV/LLf4ZNBxl45pbD4qg5ukRk26CTlqOYF/+q12jxg+1F3i//9y
0z8tHGzCAg/DHG3zU8edkVAC49GkGZPQXYleyv0+LjfDq4wfOcMJ2FvMMsFg/Ugf
E2e16APgaAW7fHxfpFGqAcGR+cHp7EN0Na8IaXqpLDLVLBtIEvaX2xcLfApLBanF
9FHOAjKGE2ScIKZ0C4G6nRvNUXSV7HWSCw1Uhyep8Po/OHU4mmY=
=b5VF
-----END PGP SIGNATURE-----
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.