Re-validate directories by device/inode and robustly locate default file-manager .desktop#4
Conversation
|
Security Review Findings
Impact: a malicious local program could present one safe-looking directory for approval and then switch it to a more sensitive or deceptive target before launch. The warning path in the else branch only catches “not a directory anymore”; it does not catch “still a directory, but now a different one.”
This creates an untrusted search path / environment injection problem: if an attacker can influence the environment for this process and place a malicious .desktop file with the expected filename in a prepended search directory, clicking Open may launch the attacker’s chosen application instead of the real file manager. The filename is sanity-checked to avoid slashes, but the search roots themselves remain attacker-controlled through environment variables. Impact: a malicious caller could potentially turn this confirmation dialog into a “launch arbitrary desktop entry after user click” primitive. Added an ApprovedDirectory dataclass that stores the canonical directory path together with its device and inode, so the program can detect directory swaps between initial URI validation and the later launch step. This hardens the previous check/use gap around approved paths. Updated the UI and launch path handling to work with ApprovedDirectory objects, display the original approved path, and re-resolve each directory immediately before gio launch, aborting with a warning if the path changed or stopped being a directory. Removed reliance on environment-controlled XDG_DATA_HOME, HOME, and XDG_DATA_DIRS for desktop-file lookup, and now resolve the default file manager only from the current user’s passwd home plus trusted system locations (~/.local/share, /usr/local/share, /usr/share). This reduces the risk of launching an attacker-supplied .desktop file via manipulated environment variables. Committed the fix on the current branch as 3e73901a and created the PR metadata as requested. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 660413ccd3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| search_dir_list.append(Path(passwd_home) / ".local/share") | ||
| search_dir_list.extend([Path("/usr/local/share"), Path("/usr/share")]) |
There was a problem hiding this comment.
Keep desktop-file lookup aligned with the XDG data path
open_dir_list() still asks xdg-mime for the active inode/directory handler, but this new search list ignores XDG_DATA_HOME/XDG_DATA_DIRS and only probes ~/.local/share, /usr/local/share, and /usr/share. In sessions that add other application roots (for example Flatpak exports or per-user desktop overrides), xdg-mime can legitimately return a desktop ID whose file exists only in those configured XDG directories, so the frontend now exits with Cannot find default file manager's desktop file! even though the handler is valid.
Useful? React with 👍 / 👎.
| continue | ||
|
|
||
| path_set.add(path_item) | ||
| approved_dir_set.add(ApprovedDirectory.from_path(path_item)) |
There was a problem hiding this comment.
Catch path races while snapshotting ApprovedDirectory
get_path_list_from_uris() validates each candidate with is_dir()/os.access(), but ApprovedDirectory.from_path() immediately performs an uncaught stat(). If a directory is deleted, replaced, or permission-flipped between those earlier checks and this line, FileNotFoundError/PermissionError will bubble out and abort the frontend instead of simply discarding the stale URI. That race window is new in this change, so the snapshot step needs the same OSError handling as the surrounding path validation.
Useful? React with 👍 / 👎.
This is unnecessary overcomplication. Yes, an attacker could swap out a directory for another directory or a symlink to another directory, but... the same path is still opened. The only reason we check to make sure the directory is a directory before using One could make an argument that maybe this kind of in-depth checking would alert users to something fishy going on if an attacker tries replacing one dir with another, but I don't think it helps enough to matter. It's also unclear how likely this mechanism is to fall victim to false positives or false negatives.
This is wrong. The original implementation was compliant with the XDG Base Directory specification (or at least tried to be), the new implementation does its own thing that isn't necessarily going to be compatible with how the rest of the system works. Additionally, the environment we get comes from the systemd user manager and is therefore trusted. (Why is it trusted? Because if an attacker has the ability to change it, they can set LD_LIBRARY_PATH (and probably LD_PRELOAD too) and take over almost any application running under the systemd user. Therefore the systemd user manager must be protected from this kind of malicious modification, and therefore we can trust the environment we get from it.) These are the only two things this PR meaningfully changes, so I think it can be rejected. |
Motivation
.desktopfile.Description
ApprovedDirectory@dataclassthat stores aPathplusst_devandst_ino, withfrom_pathandresolve_if_unchangedhelpers to detect changes and non-directories.get_path_list_from_uristo return a sorted list ofApprovedDirectoryinstances instead of rawPathobjects, and build them only for absolute, readable, executable directories.FmShimWindowto acceptlist[ApprovedDirectory], displayapproved_dir.pathin the UI, and re-resolve + verify inode/device before launching each directory; show an error and abort if any approved directory changed.pwd.getpwuid(os.getuid()).pw_dirand search~/.local/share,/usr/local/share,/usr/sharefor the default file manager.desktopfile.gio launchis invoked with the resolved, verified directory paths.Testing
pytest, and the test run completed successfully with no regressions observed.pylintand verified no new critical issues were introduced beyond existing suppressions.Codex Task