Описание
OnionShare follows symlinks in shared directories, allowing unintended disclosure of local files
Summary
OnionShare CLI/Desktop 2.6.3 can follow symbolic links inside a selected Share or Website directory and serve the symlink target rather than limiting access to files physically contained in the selected directory. If a user shares a directory that contains attacker-supplied or otherwise untrusted symlinks, a remote recipient with access to the OnionShare service can read arbitrary local files readable by the OnionShare process that the symlink points to.
This affects the shipped onionshare-cli Python package and the desktop application because both call the same onionshare_cli.web file-indexing and streaming code.
Details
Tested repository: https://github.com/onionshare/onionshare at commit 8cc75e1d7e88bd31f7276733449d412bf71c8999.
Affected product evidence:
cli/pyproject.tomldeclaresonionshare_cliversion2.6.3.desktop/pyproject.tomldeclaresonionshareversion2.6.3and depends ononionshare_clifrom../cli.cli/setup.pypublishesonionshare-cliand includesonionshare_cli.webplus templates/static resources.desktop/setup.pypublishesonionshareand exposes bothonionshareandonionshare-cliconsole scripts.
Reachable default/common paths:
- CLI share mode is the default mode when no
--receive,--website, or--chatflag is provided (cli/onionshare_cli/__init__.py:234-241) and accepts filesystem paths from CLI arguments (cli/onionshare_cli/__init__.py:184-189). - CLI website mode is exposed through
--website(cli/onionshare_cli/__init__.py:55-59). - Desktop Website mode calls
self.web.website_mode.set_file_info(self.filenames)before starting (desktop/onionshare/tab/mode/website_mode/__init__.py:281-287). Desktop Share mode callsself.mode.web.share_mode.set_file_info(...)before serving (desktop/onionshare/tab/mode/share_mode/threads.py:42-49). - Documentation describes Website mode as selecting files/folders and serving them over OnionShare (
docs/source/features.rst:109-115).
Root cause:
SendBaseModeWeb.set_file_info()expands a single selected directory into immediate children and recordsos.path.isfile()entries without rejecting symlinks (cli/onionshare_cli/web/send_base_mode.py:73-80,cli/onionshare_cli/web/send_base_mode.py:100-130). On POSIX,os.path.isfile()follows a symlink to a regular file.- Website mode serves any mapped file path through
stream_individual_file()(cli/onionshare_cli/web/website_mode.py:71-97).stream_individual_file()opens the mapped filesystem path directly (cli/onionshare_cli/web/send_base_mode.py:199-313, especiallyopen(file_to_download, "rb")at line 237). - Share mode default
/downloadcan include a symlink target when a single selected folder is expanded into root entries andZipWriter.add_file()callsself.z.write(filename, ...)without rejecting symlinks (cli/onionshare_cli/web/share_mode.py:473-539,cli/onionshare_cli/web/share_mode.py:574-580). - Share mode with
--no-autostop-sharingenables individual file downloads (cli/onionshare_cli/web/share_mode.py:122-125) and reaches the same direct streaming sink (cli/onionshare_cli/web/share_mode.py:430-447). - A partial mitigation exists only for recursive ZIP directory traversal:
ZipWriter.add_dir()skipsos.path.islink(full_filename)(cli/onionshare_cli/web/share_mode.py:582-600). That mitigation does not cover Website mode, Share individual downloads, or Share ZIP generation for root-level symlinks after the single-folder expansion path.
False-positive screening performed:
- URL path traversal without a mapped entry returned 404; the issue is not raw
../traversal but symlink following after a selected directory has been indexed. - Jinja/template escaping and CSP do not mitigate this because the sink is file read/streaming.
- Share mode has a symlink skip in recursive ZIP addition, but local PoC confirmed sibling paths still dereference symlinks.
- The default non-public onion service requires the recipient to know the onion address and private key unless the user opts into public mode; this limits exposure but does not prevent disclosure to an authorized recipient or to anyone with the URL/key.
Affected versions / patched versions:
- Affected versions: unknown; confirmed in version
2.6.3at commit8cc75e1d7e88bd31f7276733449d412bf71c8999. Earlier versions were not tested during this audit. - Patched versions: 2.6.4
Severity:
- Rationale:
AV:Nbecause the file is exposed over the OnionShare HTTP service;AC:Hbecause exploitation requires the victim to share a directory containing an attacker-influenced or untrusted symlink to a sensitive local file;PR:Lbecause the attacker generally needs the OnionShare URL/private key unless the user intentionally runs public mode;UI:Rbecause the OnionShare user must select/start sharing the affected directory;S:Ubecause the same local process reads and serves the file;C:Hbecause the symlink can point at high-value readable local secrets;I:N/A:Nbecause the confirmed impact is unintended read/disclosure.
PoC
The following safe local proof uses only temporary files and Flask's local test client. In this audit environment, several runtime dependencies were absent (waitress, flask_compress, flask_socketio, unidecode, stem, qrcode), so the harness stubbed those imports while executing the real send_base_mode, website_mode, and share_mode vulnerable code paths. No external network traffic was sent and no real secrets were read.
Maintainer reproduction from a clean checkout with normal dependencies can omit the import stubs and run the same object/test-client setup, or can start OnionShare locally with a temporary directory containing the symlink.
Positive setup and trigger:
Observed output from this environment after re-running the proof after drafting:
Negative/control case:
- Requesting
/missing.txtin Website mode returned HTTP 404 and did not containOUTSIDE_SECRET_MARKER, showing the proof is not a general path traversal or test harness artifact. - Recursive ZIP traversal has a partial control mitigation in
ZipWriter.add_dir()(cli/onionshare_cli/web/share_mode.py:593-596), but the proven variants bypass that sibling mitigation through root-level single-folder expansion and direct file streaming.
Cleanup:
- The PoC deletes the temporary directory with
shutil.rmtree(base); the audit run printedcleanup_done: true.
Impact
A remote recipient of an OnionShare Share or Website service can obtain files outside the selected shared directory if that directory contains a symlink to a readable local file. This can disclose SSH keys, browser profile data, wallet files, documents, or other local secrets readable by the OnionShare process.
The most realistic attack is a malicious archive/project/export supplied to the OnionShare user that contains a symlink to a sensitive path. If the user extracts or otherwise obtains that directory and shares it with OnionShare, the recipient can request the symlink path through Website mode, Share individual-file mode, or receive it inside the default Share ZIP in the single-folder root-symlink case.
The issue is especially surprising because one ZIP code path already tries to skip symlinks recursively, which suggests symlink dereference outside the shared tree is not intended.
Suggested remediation
Reject symlinks and enforce canonical containment both when building the file map and immediately before opening/streaming/zipping a file:
- In
SendBaseModeWeb.set_file_info(), do not add paths whereos.path.islink(path)is true. - Track selected root directories as canonical
Path.resolve()values and require every served/zipped file's resolved path to remain under one of those roots. - Re-check the resolved path immediately before
open()instream_individual_file()and beforeZipWriter.add_file()/ZipWriter.add_dir()to avoid symlink-swap/TOCTOU surprises. - Add regression tests for:
- Website mode serving a root-level symlink.
- Website mode serving a nested symlink.
- Share default
/downloadZIP for a single selected folder containing a symlink. - Share
--no-autostop-sharingindividual-file symlink download. - Existing recursive ZIP symlink skip behavior.
Пакеты
onionshare-cli
< 2.6.4
2.6.4
Связанные уязвимости
[OnionShare follows symlinks in shared directories, allowing unintended disclosure of local files]
OnionShare is an open source tool that lets you securely and anonymously share files, host websites, and chat with friends using the Tor network. Prior to 2.6.4, OnionShare CLI/Desktop follows symbolic links in cli/onionshare_cli/web/send_base_mode.py through SendBaseModeWeb.set_file_info() and stream_individual_file(), allowing remote recipients of Share or Website mode to read local files outside the selected directory. This issue is fixed in version 2.6.4.
OnionShare is an open source tool that lets you securely and anonymous ...