Skip to content

UI labels every shell-script connection with an unrelated app's name/icon (.desktop parser lets Exec=sh entries claim /usr/bin/sh, /usr/bin/dash, /usr/bin/bash) #1675

Description

@Cthululz

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:

  1. _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.

  2. _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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions