Skip to content

Commit

Permalink
(chocolatey#3281) Add validation for cache folder permissions
Browse files Browse the repository at this point in the history
These changes introduces a new validation check to ensure that
the system cache folder that is used for storing NuGet responses
have been properly locked down to administrators.

When the directory exists, and allows modifications or creations of
files by normal user this will output a validation warning about steps
that can be taken to lock down the directory.

When the directory does not exist, this same validation check ensure
that the directory is created while only allowing Administrators to
modify, create or delete anything in the folder.
  • Loading branch information
AdmiringWorm authored and gep13 committed Jul 25, 2023
1 parent a408007 commit 583d149
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 21 deletions.
3 changes: 2 additions & 1 deletion src/chocolatey/chocolatey.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.3.3.3\build\Microsoft.CodeAnalysis.BannedApiAnalyzers.props" Condition="Exists('..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.3.3.3\build\Microsoft.CodeAnalysis.BannedApiAnalyzers.props')" />
<PropertyGroup>
Expand Down Expand Up @@ -241,6 +241,7 @@
<Compile Include="infrastructure.app\rules\ServicableMetadataRule.cs" />
<Compile Include="infrastructure.app\rules\VersionMetadataRule.cs" />
<Compile Include="infrastructure.app\services\RuleService.cs" />
<Compile Include="infrastructure.app\validations\CacheFolderValidationLockdown.cs" />
<Compile Include="infrastructure\commands\ExitCodeDescription.cs" />
<Compile Include="infrastructure\cryptography\DefaultEncryptionUtility.cs" />
<Compile Include="infrastructure\adapters\IEncryptionUtility.cs" />
Expand Down
7 changes: 4 additions & 3 deletions src/chocolatey/infrastructure.app/ApplicationParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ public static class ApplicationParameters
System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile, System.Environment.SpecialFolderOption.DoNotVerify)
: CommonAppDataChocolatey;
public static readonly string HttpCacheUserLocation = _fileSystem.CombinePaths(UserProfilePath, ".chocolatey", "http-cache");
// CommonAppDataChocolatey is always set to ProgramData\Chocolatey.
// So we append HttpCache to that name if it is possible.
public static readonly string HttpCacheSystemLocation = CommonAppDataChocolatey + "HttpCache";
public static readonly string HttpCacheLocation = GetHttpCacheLocation();

public static readonly string UserLicenseFileLocation = _fileSystem.CombinePaths(UserProfilePath, "chocolatey.license.xml");
Expand Down Expand Up @@ -112,9 +115,7 @@ private static string GetHttpCacheLocation()
{
if (ProcessInformation.IsElevated() || string.IsNullOrEmpty(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile, System.Environment.SpecialFolderOption.DoNotVerify)))
{
// CommonAppDataChocolatey is always set to ProgramData\Chocolatey.
// So we append HttpCache to that name if it is possible.
return CommonAppDataChocolatey + "HttpCache";
return HttpCacheSystemLocation;
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ public void RegisterDependencies(IContainerRegistrator registrator, ChocolateyCo

registrator.RegisterService<IValidation>(
typeof(GlobalConfigurationValidation),
typeof(SystemStateValidation));
typeof(SystemStateValidation),
typeof(CacheFolderLockdownValidation));

// Rule registrations
registrator.RegisterService<IRuleService, RuleService>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright © 2017 - 2023 Chocolatey Software, Inc
// Copyright © 2011 - 2017 RealDimensions Software, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace chocolatey.infrastructure.app.validations
{
using System;
using System.Collections.Generic;
using chocolatey.infrastructure.app.configuration;
using chocolatey.infrastructure.filesystem;
using chocolatey.infrastructure.information;
using chocolatey.infrastructure.validations;

public sealed class CacheFolderLockdownValidation : IValidation
{
private readonly IFileSystem _fileSystem;

public CacheFolderLockdownValidation(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}

public ICollection<ValidationResult> Validate(ChocolateyConfiguration config)
{
this.Log().Debug("Cache Folder Lockdown Checks:");

var result = new List<ValidationResult>();

if (!ProcessInformation.IsElevated() && !string.IsNullOrEmpty(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify)))
{
this.Log().Debug(" - Elevated State = Failed");

result.Add(new ValidationResult
{
ExitCode = 0,
Message = "User Cache directory is valid.",
Status = ValidationStatus.Success
});

return result;
}

this.Log().Debug(" - Elevated State = Checked");

var cacheFolderPath = ApplicationParameters.HttpCacheLocation;

if (_fileSystem.DirectoryExists(cacheFolderPath))
{
this.Log().Debug(" - Folder Exists = Checked");

if (_fileSystem.IsLockedDirectory(cacheFolderPath))
{
this.Log().Debug(" - Folder lockdown = Checked");

result.Add(new ValidationResult
{
ExitCode = 0,
Message = "System Cache directory is locked down to administrators.",
Status = ValidationStatus.Success
});
}
else
{
this.Log().Debug(" - Folder lockdown = Failed");

result.Add(new ValidationResult
{
ExitCode = 0,
Message = "System Cache directory is not locked down to administrators. Remove the directory '{0}' to have Chocolatey CLI create it with the proper permissions.".FormatWith(cacheFolderPath),
Status = ValidationStatus.Warning
});
}

return result;
}

this.Log().Debug(" - Folder Exists = Failed");

if (_fileSystem.LockDirectory(cacheFolderPath))
{
this.Log().Debug(" - Folder lockdown update = Success");

result.Add(new ValidationResult
{
ExitCode = 0,
Message = "System Cache directory successfullly created and locked down to administrators.",
Status = ValidationStatus.Success,
});
}
else
{
this.Log().Debug(" - Folder lockdown update = Failed");

result.Add(new ValidationResult
{
ExitCode = 1, // Should we error?
Message = "System Cache directory was not created, or could not be locked down to administrators.",
Status = ValidationStatus.Error
});
}

return result;
}

#pragma warning disable IDE1006

[Obsolete("This overload is deprecated and will be removed in v3.")]
public ICollection<ValidationResult> validate(ChocolateyConfiguration config)
{
return Validate(config);
}

#pragma warning restore IDE1006
}
}
142 changes: 126 additions & 16 deletions src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ namespace chocolatey.infrastructure.filesystem
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Threading;
using adapters;
Expand Down Expand Up @@ -646,19 +648,7 @@ public dynamic GetFileDirectoryInfo(string filePath)

public void CreateDirectory(string directoryPath)
{
this.Log().Debug(ChocolateyLoggers.Verbose, () => "Attempting to create directory \"{0}\".".FormatWith(GetFullPath(directoryPath)));
AllowRetries(
() =>
{
try
{
Directory.CreateDirectory(directoryPath);
}
catch (IOException)
{
Alphaleonis.Win32.Filesystem.Directory.CreateDirectory(directoryPath);
}
});
CreateDirectory(directoryPath, isSilent: false);
}

public void MoveDirectory(string directoryPath, string newDirectoryPath)
Expand Down Expand Up @@ -762,19 +752,139 @@ public void EnsureDirectoryExists(string directoryPath)
EnsureDirectoryExists(directoryPath, false);
}

private void EnsureDirectoryExists(string directoryPath, bool ignoreError)
public bool IsLockedDirectory(string directoryPath)
{
try
{
var permissions = Directory.GetAccessControl(directoryPath);

var rules = permissions.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(NTAccount));
var builtinAdmins = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount));
var localSystem = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null).Translate(typeof(NTAccount));

foreach (FileSystemAccessRule rule in rules)
{
if (rule.IdentityReference != builtinAdmins && rule.IdentityReference != localSystem &&
AllowsAnyFlag(rule, FileSystemRights.CreateFiles, FileSystemRights.AppendData, FileSystemRights.WriteExtendedAttributes, FileSystemRights.WriteAttributes))
{
return false;
}
}

return true;
}
catch (Exception ex)
{
this.Log().Debug("Unable to read directory '{0}'.", directoryPath);
this.Log().Debug("Exception: {0}", ex.Message);

// If we do not have access, we assume the directory is locked.
return true;
}
}

public bool LockDirectory(string directoryPath)
{
try
{
EnsureDirectoryExists(directoryPath, ignoreError: false, isSilent: true);

this.Log().Debug(" - Folder Created = Success");

var permissions = Directory.GetAccessControl(directoryPath);

var rules = permissions.GetAccessRules(includeExplicit: true, includeInherited: true, typeof(NTAccount));

// We first need to remove all rules
foreach (FileSystemAccessRule rule in rules)
{
permissions.RemoveAccessRuleAll(rule);
}

this.Log().Debug(" - Pending normal access removed = Checked");

var bultinAdmins = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Translate(typeof(NTAccount));
var localsystem = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null).Translate(typeof(NTAccount));
var builtinUsers = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null).Translate(typeof(NTAccount));

var inheritanceFlags = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;

permissions.SetAccessRule(new FileSystemAccessRule(bultinAdmins, FileSystemRights.FullControl, inheritanceFlags, PropagationFlags.None, AccessControlType.Allow));
permissions.SetAccessRule(new FileSystemAccessRule(localsystem, FileSystemRights.FullControl, inheritanceFlags, PropagationFlags.None, AccessControlType.Allow));
permissions.SetAccessRule(new FileSystemAccessRule(builtinUsers, FileSystemRights.ReadAndExecute, inheritanceFlags, PropagationFlags.None, AccessControlType.Allow));
this.Log().Debug(" - Pending write permissions for Administrators = Checked");

permissions.SetOwner(bultinAdmins);
this.Log().Debug(" - Pending Administrators as Owners = Checked");

permissions.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
this.Log().Debug(" - Pending removing inheritance with no copy = Checked");

Directory.SetAccessControl(directoryPath, permissions);
this.Log().Debug(" - Access Permissions updated = Success");

return true;
}
catch (Exception ex)
{
this.Log().Debug(" - Access Permissions updated = Failure");
this.Log().Debug("Exception: {0}", ex.Message);

return false;
}
}

private bool AllowsAnyFlag(FileSystemAccessRule rule, params FileSystemRights[] flags)
{
foreach (var flag in flags)
{
if (rule.AccessControlType == AccessControlType.Allow && rule.FileSystemRights.HasFlag(flag))
{
return true;
}
}

return false;
}

private void CreateDirectory(string directoryPath, bool isSilent)
{
if (!isSilent)
{
this.Log().Debug(ChocolateyLoggers.Verbose, () => "Attempting to create directory \"{0}\".".FormatWith(GetFullPath(directoryPath)));
}

AllowRetries(
() =>
{
try
{
Directory.CreateDirectory(directoryPath);
}
catch (IOException)
{
Alphaleonis.Win32.Filesystem.Directory.CreateDirectory(directoryPath);
}
}, isSilent: isSilent);
}

private void EnsureDirectoryExists(string directoryPath, bool ignoreError, bool isSilent = false)
{
if (!DirectoryExists(directoryPath))
{
try
{
CreateDirectory(directoryPath);
CreateDirectory(directoryPath, isSilent);
}
catch (SystemException e)
{
if (!ignoreError)
{
this.Log().Error("Cannot create directory \"{0}\". Error was:{1}{2}", GetFullPath(directoryPath), Environment.NewLine, e);
if (!isSilent)
{
this.Log().Error("Cannot create directory \"{0}\". Error was:{1}{2}", GetFullPath(directoryPath), Environment.NewLine, e);
}

throw;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/chocolatey/infrastructure/filesystem/IFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ public interface IFileSystem
/// <param name="isSilent">Should this method be silent? false by default</param>
void DeleteDirectoryChecked(string directoryPath, bool recursive, bool overrideAttributes, bool isSilent);

bool IsLockedDirectory(string directoryPath);

bool LockDirectory(string directoryPath);

#endregion

/// <summary>
Expand Down

0 comments on commit 583d149

Please sign in to comment.