Skip to content

Security hardening: medium-severity fixes#612

Merged
mehmetozguldev merged 8 commits intoathasdev:masterfrom
Finesssee:security/hardening
Apr 16, 2026
Merged

Security hardening: medium-severity fixes#612
mehmetozguldev merged 8 commits intoathasdev:masterfrom
Finesssee:security/hardening

Conversation

@Finesssee
Copy link
Copy Markdown
Contributor

This PR implements the remaining medium-severity security hardening tasks (M1, M2, M5) following the high-severity fixes already merged.

Changes

M5 - Quote dirname substitutions in SSH shell helpers

  • Fixed , , and in
  • Quoted to prevent shell injection when dirname output contains spaces or globs

M2 - Sanitize formatter/linter command and environment variables

  • Added with shared validation
  • Rejects loader-hijack environment variables (LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, LD_DEBUG, DYLD_*, PATH)
  • Rejects relative paths and commands with ".." components
  • Allows bare command names (resolved via PATH) and absolute paths
  • Integrated into and IPC commands
  • Added 7 regression tests

M1 - Confine custom fs IPC commands to home directory

  • Added with path validation
  • Canonicalizes paths and enforces containment under (matching Tauri's fs:scope)
  • Added for general path validation
  • Added for symlink inspection without following
  • Applied to , , ,
  • Added 5 regression tests including symlink escape detection

Verification

  • Rust checks pass
  • TypeScript typecheck passes
  • Rust unit tests pass (7 exec_guard tests, 5 path_guard tests)

The previous allowlist used host.ends_with("athas.dev"), which also matched
hostnames like evilathas.dev because the suffix check did not require a
leading dot. An attacker who can register such a domain could serve a
malicious extension bundle that download_extension and
install_extension_from_url would accept.

Tighten the check to host == "athas.dev" || host.ends_with(".athas.dev"),
and add a regression test covering evilathas.dev, bare athas.dev, and
cdn.athas.dev.
extract_tar_gz manually joined each tar entry's relative path onto the
target directory without rejecting ".." components, while Path::join does
not normalize traversal. A tampered or malicious mirror of nodejs.org
could ship a tar entry whose resolved path escapes the runtime install
directory, writing arbitrary files including executables during install.

Stage the archive in a tempdir, unpack each entry via
tar::Entry::unpack_in which rejects unsafe paths, then promote the single
wrapper directory into target_dir. This matches the pattern already used
by the extensions and tool installers.

Add a regression test that constructs a two-step symlink tar-slip and
confirms extraction fails before any payload escapes the staging dir.
download_binary previously fetched whatever URL a ToolConfig supplied,
with no scheme or host checks and no size limit, then wrote the result
into tools_dir/bin and marked it executable. Because ToolConfig flows
through the install_language_tools IPC command, this was a
frontend-to-RCE primitive if any untrusted input ever reached the tool
config path.

Require HTTPS URLs, allow http://localhost only in debug builds, cap the
download at 100 MB using both the Content-Length hint and a running
counter over the streamed body, and surface failures as typed
ToolError::DownloadFailed. Add unit tests for the URL validator covering
non-HTTPS schemes, localhost in debug, and the happy path.
Capture the full security audit in SECURITY-REVIEW.md: severity rubric,
findings table, and details for the three high-severity fixes applied in
this branch plus medium and low items tracked for follow-up.
ssh_create_file, ssh_rename_path, and ssh_copy_path built remote commands
like `mkdir -p $(dirname 'target')` where the single-quoted argument was
safe inside dirname, but the outer command substitution was unquoted.
dirname's output does not contain shell metacharacters in normal use, but
an unusual remote path (spaces, glob characters) could produce an argv
mismatch or unintended word-splitting in the outer mkdir.

Wrap each command substitution in double quotes so mkdir always receives
a single argument regardless of the resolved directory name.
format_code and lint_code accept a FormatterConfig or LinterConfig over
IPC whose command, args, and env values are passed straight through to
std::process::Command. The frontend is trusted in the current threat
model, but a malicious or mis-configured extension could previously
supply an absolute path to a surprise binary, a relative path resolved
against the editor's CWD, or an LD_PRELOAD-style environment override
that hijacks the child process.

Add a shared exec_guard module that rejects commands containing ".." or
relative path separators while allowing bare names (looked up via PATH)
and explicit absolute paths, and rejects environment variables known to
redirect the dynamic linker or library search path on Linux and macOS
(LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT, LD_DEBUG, DYLD_*, and PATH
itself). The check is case-insensitive to cover Windows conventions.

Wire the guard into format_with_generic and lint_with_generic so invalid
configs fail fast with a typed error before any template substitution or
process spawn happens.
open_file_external, get_symlink_info, rename_file, and move_file call
std::fs and std::process directly and therefore bypass Tauri's fs:scope
capability. That capability is configured as $HOME/** in
capabilities/main.json, so any path the frontend hands to these commands
could previously operate on system locations (for example /etc/passwd)
or, in the case of open_file_external on Linux, be interpreted by
xdg-open as a URL and delegated to the system handler.

Add a small path_guard module with two helpers. require_path_under_home
canonicalizes the input (or its parent for not-yet-existing targets) and
rejects anything outside the canonicalized $HOME, so symlinks pointing
out of the workspace are caught. require_symlink_container_under_home
checks the parent directory only, which keeps get_symlink_info able to
report on symlinks inside $HOME whose target is elsewhere.

Wire both helpers into fs.rs so every path argument goes through the
guard before reaching the filesystem or the platform opener. Include
regression tests for acceptance inside $HOME, rejection outside $HOME,
and rejection of symlinks that escape $HOME.
@mehmetozguldev mehmetozguldev self-requested a review April 16, 2026 18:31
Copy link
Copy Markdown
Member

@mehmetozguldev mehmetozguldev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so good and it covers A LOT of things. Thank you so much, merging it

@mehmetozguldev mehmetozguldev merged commit 75e9763 into athasdev:master Apr 16, 2026
1 of 2 checks passed
@Finesssee
Copy link
Copy Markdown
Contributor Author

The power of Opus 4.7 LOL

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants