| File: parent\dotnet\Installer\Windows\TrustedMachinePath.cs | Web Access |
| Project: dotnet-aot.csproj (dotnet-aot) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.Versioning; using System.Security.AccessControl; using System.Security.Principal; namespace Microsoft.DotNet.Cli.Installer.Windows; /// <summary> /// Establishes that the machine-wide dotnet directory can be trusted by elevated code. The default /// <c>%ProgramData%</c> descriptor lets a standard user create a top-level directory, so an object's /// location alone does not make it trustworthy. /// </summary> /// <remarks> /// <para> /// Only the root is validated. Once the root grants modification rights to no principal outside /// <see cref="s_trustedSids"/>, an unprivileged user cannot create, replace, rename, or delete /// anything below it, so descendants are trusted by induction. Validating each descendant again /// would cost a descriptor read per component without excluding any additional attacker. /// </para> /// <para> /// Trust is evaluated by capability rather than by exact descriptor equality: a trusted owner, /// no reparse point, and no modification rights outside the trusted principals. Additional read-only /// allow rules and restrictive deny rules are permitted so enterprise ACL customization does not /// break workload operations. /// </para> /// </remarks> #if NET [SupportedOSPlatform("windows")] #endif internal static class TrustedMachinePath { private static readonly SecurityIdentifier s_administratorsSid = new(WellKnownSidType.BuiltinAdministratorsSid, null); private static readonly SecurityIdentifier s_localSystemSid = new(WellKnownSidType.LocalSystemSid, null); private static readonly HashSet<SecurityIdentifier> s_trustedSids = [s_administratorsSid, s_localSystemSid]; /// <summary> /// Rights that let a principal alter an object, its children, or its security descriptor. /// <see cref="FileSystemRights.DeleteSubdirectoriesAndFiles"/> on a directory removes a child /// whatever the child's own descriptor says, which is why directory rights are the check that /// matters rather than the rights on an individual file. /// </summary> private const FileSystemRights ModificationRights = FileSystemRights.WriteData | FileSystemRights.AppendData | FileSystemRights.WriteExtendedAttributes | FileSystemRights.WriteAttributes | FileSystemRights.Delete | FileSystemRights.DeleteSubdirectoriesAndFiles | FileSystemRights.ChangePermissions | FileSystemRights.TakeOwnership; /// <summary> /// The machine-wide dotnet directory that anchors trust for everything below it. /// </summary> public static string ProgramDataDotnetRoot { get; } = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "dotnet"); /// <summary> /// Determines whether a path resolves to <paramref name="root"/> or one of its descendants. /// </summary> public static bool IsUnderRoot(string path, string root) { string fullPath = Path.GetFullPath(path); string fullRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); return fullPath.Equals(fullRoot, StringComparison.OrdinalIgnoreCase) || fullPath.StartsWith(fullRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); } /// <summary> /// Validates <see cref="ProgramDataDotnetRoot"/> when it exists. A missing root is not an error; /// the caller creates it with the intended descriptor. /// </summary> /// <exception cref="InvalidDataException">The root cannot be trusted.</exception> public static void ValidateProgramDataRoot() { if (Directory.Exists(ProgramDataDotnetRoot)) { ValidateDirectory(ProgramDataDotnetRoot); } } /// <summary> /// Validates that a directory is not a reparse point and that its descriptor is trusted. /// </summary> public static void ValidateDirectory(string path) { Validate(new DirectoryInfo(path).GetAccessControl(AccessControlSections.Access | AccessControlSections.Owner), path); if ((File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0) { ThrowUntrusted(path); } } /// <summary> /// Validates that a descriptor has a trusted machine owner, grants full control to Administrators /// and SYSTEM, and grants no modification rights to any other principal. /// </summary> public static void Validate(FileSystemSecurity security, string path) { if (security.GetOwner(typeof(SecurityIdentifier)) is not SecurityIdentifier owner || !s_trustedSids.Contains(owner)) { ThrowUntrusted(path); } AuthorizationRuleCollection rules = security.GetAccessRules( includeExplicit: true, includeInherited: true, typeof(SecurityIdentifier)); var trustedRights = new Dictionary<SecurityIdentifier, FileSystemRights>(); foreach (FileSystemAccessRule rule in rules) { if (rule.AccessControlType == AccessControlType.Deny) { continue; } if (rule.IdentityReference is not SecurityIdentifier sid) { ThrowUntrusted(path); continue; } if (s_trustedSids.Contains(sid)) { trustedRights.TryGetValue(sid, out FileSystemRights existingRights); trustedRights[sid] = existingRights | rule.FileSystemRights; } else if ((rule.FileSystemRights & ModificationRights) != 0) { // Inherit-only rules count here: they become effective rights on descendants. ThrowUntrusted(path); } } foreach (SecurityIdentifier trustedSid in s_trustedSids) { if (!trustedRights.TryGetValue(trustedSid, out FileSystemRights rights) || (rights & FileSystemRights.FullControl) != FileSystemRights.FullControl) { ThrowUntrusted(path); } } } private static void ThrowUntrusted(string path) => throw new InvalidDataException( $"'{path}' has unexpected permissions and cannot be trusted. Delete '{path}' and retry."); }