fix(security): walk parent components + reject reparse points in dir_is_privileged (#712)#48
Merged
Merged
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
…is_privileged (#712) The shared privileged-owner gate (self-heal #565, TLS root #661, service install #46) checked only the LEAF directory's owner. A privileged-owned leaf under a user-writable — or symlinked/junctioned — ancestor is still tamperable: an intermediate rename/replace is governed by the parent's permissions, and a reparse point anywhere redirects the whole path. dir_is_privileged now walks EVERY existing ancestor component and holds each to the same bar, rejecting any symlink/junction/reparse component (no-follow symlink_metadata + FILE_ATTRIBUTE_REPARSE_POINT on Windows). Accepts the well-known NT SERVICE\TrustedInstaller SID that owns C:\Program Files so the canonical %ProgramFiles%\DIG\bin install root is not false-rejected. Aligns with dig-dns #701. Fails closed. One fix hardens all three gates. Refs #46 #565 #661 #701 Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
marked this pull request as ready for review
July 17, 2026 06:08
Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What + why (dig_ecosystem #712)
Hardens the shared privileged-owner trust gate
crate::security::dir_is_privileged(
crates/dig-node-service/src/security.rs), which backs THREE gates: self-heal spawn root (#565),TLS material root (#661), and the system-service install target (#46). It previously checked only the
LEAF directory's owner. A privileged-owned leaf under a user-writable — or symlinked/junctioned —
ancestor is still tamperable: an intermediate rename/replace is governed by the parent's
permissions, and a reparse point anywhere redirects the whole path. Aligns dig-node with the higher
bar dig-dns already ships (#701).
Change
dir_is_privilegednow walks EVERY existing ancestor component (leaf → parent → … → filesystem root)and holds each to the same bar; a single weak component fails the whole check. Per component:
root/uid 0, no group/other write bit, and NOT a symlink (no-followsymlink_metadata/lstat so a symlink is judged on its own identity).GetNamedSecurityInfoW) equal toS-1-5-18,S-1-5-32-544, orNT SERVICE\TrustedInstaller, AND not a reparse point(
FILE_ATTRIBUTE_REPARSE_POINTvia no-followsymlink_metadata, which also catches junctions).Fails CLOSED on any indeterminate/missing component. One fix hardens all three gates (they share the
helper).
No false-reject of the canonical path (the key risk)
The canonical Windows install root is
%ProgramFiles%\DIG\bin. Its ancestorC:\Program Filesisowned by TrustedInstaller, NOT SYSTEM/Administrators — so a naive ancestor-walk accepting only
SYSTEM/Administrators would false-reject the legitimate path. The fix accepts the fixed, byte-identical
NT SERVICE\TrustedInstallerservice SID (privileged: a non-admin cannot write it without takingownership). Ancestors resolve as:
…\bin/…\DIG(Administrators/SYSTEM) →C:\Program Files(TrustedInstaller, now accepted) →
C:\(SYSTEM) — all pass, none reparse.Blast radius (gitnexus + manual)
dir_is_privilegedhas exactly 3 callers, all pass a single dir expectingtruefor a privilegedpath — all now get the tighter whole-path check for free:
self_heal::resolve_privileged_sibling_in(#565 spawn root)service::ensure_service_target_is_safe(fix(service): gate system-level install against a user-writable binary (#565 LPE) #46 system-install gate)tls::load_https_material(#661 TLS root)Tests (TDD)
unix_component_policy_requires_root_owned_non_writable_non_symlink— pure per-component policy incl. symlink rejection even when root-owned.a_symlink_component_is_never_privileged— real-fs: a symlink leaf is refused (reparse rejection).a_child_under_a_user_writable_parent_is_not_privileged— real-fs: the walk reaches ancestors; a non-writable child under a world-writable parent is refused.windows_owner_policy_accepts_the_trusted_installer_sid— TrustedInstaller SID accepted (regression against the false-reject).windows_reparse_attribute_is_detected— pureFILE_ATTRIBUTE_REPARSE_POINTbit test.Gates (local, Windows,
CARGO_TARGET_DIRshort to dodge MAX_PATH)cargo test -p dig-node-service --libsecurity (5), service (25), self_heal (9), tls (6) — all pass.cargo fmt -p dig-node-service -- --check— clean.cargo clippy -p dig-node-service --all-targets --all-features -- -D warnings— clean.Version
0.38.0 → 0.38.1(workspace root) — patch:fix:security hardening, no public-API signaturechange (
dir_is_privilegedonly refuses MORE — previously-trusted weak paths — never breaks thecanonical privileged paths). apps/nightlies model: rides the next nightly; no stable auto-cut.
Docs
SPEC.md — added a "Shared whole-path owner check" subsection + updated the TLS/install gate wording;
DEVELOPMENT_LOG.md — durable note on the ancestor walk + the TrustedInstaller Program Files gotcha.
Pre-merge security gate
This is a §565 LPE-adjacent check — built audit-clean, fail-closed, spawn-free owner read retained.
Refs #46 #565 #661 #701