Skip to content

fix(updater): close TOCTOU and downgrade in the root Linux update helper - #581

Merged
kdroidFilter merged 1 commit into
nucleus-2.5from
security/linux-update-helper-toctou
Aug 19, 2026
Merged

fix(updater): close TOCTOU and downgrade in the root Linux update helper#581
kdroidFilter merged 1 commit into
nucleus-2.5from
security/linux-update-helper-toctou

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Security fix — F-1 / F-2 (audit 2026-08-19)

Severity: High (local privilege escalation) · Module: plugin-build/plugin (LinuxUpdateHelper)

This is the only code path in the project that reaches root.

Problem

The passwordless self-update helper is installed as a package-owned file and invoked as root through a polkit action with <allow_active>yes</allow_active> (no password for an active local session). It verified the update and then installed it via two separate opens of a caller-controlled path:

gpg --homedir "$KR" --batch --verify "$SIG" "$PKG"
...
exec dpkg -i "$PKG"     # re-opens the same, user-writable path
  • F-1 (TOCTOU → LPE): the invoking user is unprivileged relative to the root helper, but $PKG/$SIG live where that user can write them. Any process running as the desktop user can swap the file contents between gpg --verify and dpkg -i, and dpkg's maintainer scripts then run as root.
  • F-2 (signed downgrade): the helper checks only the package name, not the version. dpkg -i installs an older, still-validly-signed release, rolling the app back to a known-vulnerable version. (rpm -U already refuses downgrades.)

Fix

  1. Verify the copy, not the caller's file. The helper copies the package and signature into a root-owned mktemp -d (0700) before verifying, then verifies and installs the copies. The unprivileged caller cannot alter bytes inside a root-only directory, so the verify→install window is closed.
  2. Refuse downgrades on deb. After the name match, the deb path requires a strict version increase via dpkg --compare-versions "$NEWVER" gt "$CUR" and exits otherwise. rpm keeps rpm -U (already downgrade-refusing).

Test

LinuxUpdateHelperTest asserts the security-relevant structure of the generated script: package/signature copied into the work dir before --verify, verification runs against the in-work-dir copy, and the deb path uses dpkg --compare-versions … gt and exits on a non-upgrade — so a future refactor cannot silently drop the hardening.

Verified locally with JDK 21: :plugin:test --tests …LinuxUpdateHelperTest passes. The bash logic was reviewed, not executed here; it continues to be exercised against real distros in the packaging workflows.

🤖 Generated with Claude Code

The passwordless self-update helper runs as root via pkexec but takes a
caller-supplied package path that is writable by the unprivileged invoking
user. It verified the detached signature and then re-opened the same path
for `dpkg -i` / `rpm -U` — two separate opens, so the bytes could be swapped
between verify and install, giving a local user root code execution.

The helper now copies the package and its signature into a root-owned 0700
working dir first, then verifies and installs the copies, so the verified
bytes cannot be replaced. The deb path additionally refuses anything that is
not a strict version increase (`dpkg --compare-versions ... gt`), blocking a
rollback to an older, still-validly-signed, vulnerable release; `rpm -U`
already refuses downgrades.

The bash is exercised on real distros in packaging; a new unit test locks the
security-relevant structure (copy-before-verify, verify-the-copy, refuse
non-upgrade) so a refactor cannot silently drop it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kdroidFilter
kdroidFilter merged commit 1ba8e84 into nucleus-2.5 Aug 19, 2026
4 checks passed
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.

1 participant