Summary
The UI's .desktop-file parser can associate a shell interpreter (/usr/bin/sh, /usr/bin/dash, /usr/bin/bash, ...) with a completely unrelated application. Once that happens, every connection made by any shell script on the system — anything launched via a #!/bin/sh / #!/bin/bash shebang, or through a sh -c wrapper — is displayed in the interconnection prompt with that unrelated application's name and icon.
This mislabels the single most security-relevant dialog OpenSnitch shows: the user is asked to approve "App X phoning home" when the traffic actually belongs to an anonymous shell script. Rule matching itself is unaffected (it uses process.path / cmdline), but the human decision is being made against a wrong identity.
Root cause
In ui/opensnitch/desktop_parser.py there are two interacting behaviors:
-
_parse_exec() takes the first whitespace-delimited token of Exec= and resolves bare command names through PATH. For an entry like Exec=sh -c '...' this yields /usr/bin/sh.
-
_parse_desktop_file() then registers the entry under several keys, and additionally follows symlinks to their target:
self.apps[cmd] = (name, icon, desc, desktop_path)
self.apps[basename] = (name, icon, desc, desktop_path)
# if the command is a symlink, add the real binary too
if os.path.islink(cmd):
link_to = os.path.realpath(cmd)
self.apps[link_to] = (name, icon, desc, desktop_path)
On Debian-family systems /usr/bin/sh and /bin/sh are symlinks to dash. So any .desktop file whose Exec goes through sh claims /usr/bin/sh and /usr/bin/dash as "its" application.
Concrete distro example (Linux Mint ships this): the steam.desktop installer stub contains
Name=Install Steam
Exec=sh -c 'STEAM_FRAME_FORCE_CLOSE=1 steam %U'
The parser therefore stores apps['/usr/bin/dash'] = ('Install Steam', 'steam', ...). Every shell-script connection is then announced as "Install Steam" with Steam's icon.
The prompt dialog consumes this via get_info_by_path(connection.process_path, ...): when a process launched by a shell script is intercepted, the daemon reports the interpreter as process_path (e.g. /usr/bin/dash), the lookup hits the poisoned key, and the popup title/icon belong to the unrelated app.
Reproducing
On any Debian-family system with a Exec=sh ... desktop entry installed (the Steam installer stub makes it immediate):
# 1. inspect the lookup table directly
python3 -c "
from opensnitch.desktop_parser import LinuxDesktopParser
p = LinuxDesktopParser()
print(p.apps.get('/usr/bin/dash'))
print(p.apps.get('/usr/bin/sh'))
print(p.apps.get('/usr/bin/bash'))"
# 2. find which installed entries do the claiming
grep -l '^Exec=sh ' /usr/share/applications/*.desktop
grep -l '^Exec=bash' /usr/share/applications/*.desktop
Expected result for step 1 on a system with the Steam stub: ('Install Steam', 'steam', ...) for the dash/sh keys — nothing to do with any running process. Then trigger any connection from a #!/bin/sh script with interception enabled: the prompt shows the unrelated app name/icon.
Note there are actually two independent problems stacked here:
- Problem A (the hijack): a
.desktop entry whose Exec starts with a shell claims the shell binary as its app. Any user-installed or distro-shipped entry can do this — nothing about the Steam stub is special.
- Problem B (the amplifier):
realpath() following of symlinks (/usr/bin/sh → /usr/bin/dash) widens each hijack to the interpreter's alternative paths. This behavior is presumably meant to map symlinked app binaries to their .desktop entries, but for sh→dash it extends the mislabeling to the interpreter itself.
Suggested fix
Skip registration when the resolved command is a shell interpreter, so those lookups fall through to the default terminal icon + plain path:
_SHELL_INTERPRETERS = frozenset((
'sh', 'bash', 'dash', 'ash', 'zsh', 'ksh', 'csh', 'tcsh', 'fish'
))
# in _parse_desktop_file(), before the self.apps[...] assignments:
if os.path.basename(cmd) in _SHELL_INTERPRETERS:
return
That single guard fixes Problem A for all entries at once and is enough to defuse the real-world case. Optionally, additionally restricting the symlink-following in Problem B (e.g. never follow when the target is also an interpreter) would close the residual hole where a non-shell Exec still resolves to /usr/bin/sh some other way.
Happy to send a PR with this if the approach looks right.
Environment
- OpenSnitch UI v1.9.0
- Verified the same logic is present in current
master's ui/opensnitch/desktop_parser.py
- Linux, Debian-family distro with
/usr/bin/sh -> dash
Summary
The UI's
.desktop-file parser can associate a shell interpreter (/usr/bin/sh,/usr/bin/dash,/usr/bin/bash, ...) with a completely unrelated application. Once that happens, every connection made by any shell script on the system — anything launched via a#!/bin/sh/#!/bin/bashshebang, or through ash -cwrapper — is displayed in the interconnection prompt with that unrelated application's name and icon.This mislabels the single most security-relevant dialog OpenSnitch shows: the user is asked to approve "App X phoning home" when the traffic actually belongs to an anonymous shell script. Rule matching itself is unaffected (it uses
process.path/ cmdline), but the human decision is being made against a wrong identity.Root cause
In
ui/opensnitch/desktop_parser.pythere are two interacting behaviors:_parse_exec()takes the first whitespace-delimited token ofExec=and resolves bare command names throughPATH. For an entry likeExec=sh -c '...'this yields/usr/bin/sh._parse_desktop_file()then registers the entry under several keys, and additionally follows symlinks to their target:On Debian-family systems
/usr/bin/shand/bin/share symlinks todash. So any.desktopfile whose Exec goes throughshclaims/usr/bin/shand/usr/bin/dashas "its" application.Concrete distro example (Linux Mint ships this): the
steam.desktopinstaller stub containsThe parser therefore stores
apps['/usr/bin/dash'] = ('Install Steam', 'steam', ...). Every shell-script connection is then announced as "Install Steam" with Steam's icon.The prompt dialog consumes this via
get_info_by_path(connection.process_path, ...): when a process launched by a shell script is intercepted, the daemon reports the interpreter asprocess_path(e.g./usr/bin/dash), the lookup hits the poisoned key, and the popup title/icon belong to the unrelated app.Reproducing
On any Debian-family system with a
Exec=sh ...desktop entry installed (the Steam installer stub makes it immediate):Expected result for step 1 on a system with the Steam stub:
('Install Steam', 'steam', ...)for thedash/shkeys — nothing to do with any running process. Then trigger any connection from a#!/bin/shscript with interception enabled: the prompt shows the unrelated app name/icon.Note there are actually two independent problems stacked here:
.desktopentry whose Exec starts with a shell claims the shell binary as its app. Any user-installed or distro-shipped entry can do this — nothing about the Steam stub is special.realpath()following of symlinks (/usr/bin/sh→/usr/bin/dash) widens each hijack to the interpreter's alternative paths. This behavior is presumably meant to map symlinked app binaries to their.desktopentries, but forsh→dashit extends the mislabeling to the interpreter itself.Suggested fix
Skip registration when the resolved command is a shell interpreter, so those lookups fall through to the default terminal icon + plain path:
That single guard fixes Problem A for all entries at once and is enough to defuse the real-world case. Optionally, additionally restricting the symlink-following in Problem B (e.g. never follow when the target is also an interpreter) would close the residual hole where a non-shell Exec still resolves to
/usr/bin/shsome other way.Happy to send a PR with this if the approach looks right.
Environment
master'sui/opensnitch/desktop_parser.py/usr/bin/sh -> dash