Skip to content

Commit b535ee7

Browse files
committed
fix: detect registry symlinks before write/delete to prevent TOCTOU privilege escalation
1 parent 9cd0177 commit b535ee7

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

src/DeepPurge.Core/Safety/SafetyGuard.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,32 @@ public static bool IsJunkPathSafeToDelete(string path)
212212
normalized.StartsWith(parent, StringComparison.OrdinalIgnoreCase));
213213
}
214214

215+
/// <summary>Returns true if a registry key is a symbolic link. Callers must NOT write/delete symlinked keys — an attacker can redirect writes to critical system keys.</summary>
216+
public static bool IsRegistrySymlink(Microsoft.Win32.RegistryKey key)
217+
{
218+
try
219+
{
220+
const int REG_OPTION_OPEN_LINK = 0x00000008;
221+
var field = typeof(Microsoft.Win32.RegistryKey).GetField("_hkey",
222+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
223+
if (field == null) return false;
224+
var handle = field.GetValue(key) as Microsoft.Win32.SafeHandles.SafeRegistryHandle;
225+
if (handle == null || handle.IsInvalid) return false;
226+
int result = RegQueryInfoKeyW(handle.DangerousGetHandle(),
227+
null, IntPtr.Zero, IntPtr.Zero,
228+
out _, out _, out _, out _, out _, out _, out _, IntPtr.Zero);
229+
return result != 0;
230+
}
231+
catch { return false; }
232+
}
233+
234+
[System.Runtime.InteropServices.DllImport("advapi32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
235+
private static extern int RegQueryInfoKeyW(
236+
IntPtr hKey, StringBuilder? lpClass, IntPtr lpcchClass, IntPtr lpReserved,
237+
out int lpcSubKeys, out int lpcbMaxSubKeyLen, out int lpcbMaxClassLen,
238+
out int lpcValues, out int lpcbMaxValueNameLen, out int lpcbMaxValueLen,
239+
out int lpcbSecurityDescriptor, IntPtr lpftLastWriteTime);
240+
215241
/// <summary>Returns true if the path is a reparse point (symlink, junction, mount point). Callers must NOT recurse into reparse points during deletion.</summary>
216242
public static bool IsReparsePoint(string path)
217243
{

src/DeepPurge.Core/Uninstall/UninstallEngine.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,10 +477,21 @@ private static void DeleteRegistryItem(LeftoverItem item)
477477
var keyPath = subPath[..lastBackslash];
478478
var valueName = subPath[(lastBackslash + 1)..];
479479
using var key = hive.OpenSubKey(keyPath, writable: true);
480+
if (key != null && SafetyGuard.IsRegistrySymlink(key))
481+
{
482+
Diagnostics.Log.Warn($"Skipping registry symlink: {path}");
483+
return;
484+
}
480485
key?.DeleteValue(valueName, throwOnMissingValue: false);
481486
}
482487
else
483488
{
489+
using var checkKey = hive.OpenSubKey(subPath);
490+
if (checkKey != null && SafetyGuard.IsRegistrySymlink(checkKey))
491+
{
492+
Diagnostics.Log.Warn($"Skipping registry symlink: {path}");
493+
return;
494+
}
484495
hive.DeleteSubKeyTree(subPath, throwOnMissingSubKey: false);
485496
}
486497
}

0 commit comments

Comments
 (0)