Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SNOW-1156046] fix toctou vulnerability in EasyLogginConfig #925

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions Snowflake.Data.Tests/IntegrationTests/EasyLoggingIT.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Data;
using System.IO;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -73,13 +74,9 @@ public void TestFailToEnableEasyLoggingForWrongConfiguration()
}

[Test]
[Platform(Exclude="Win", Reason="skip test on Windows")]
public void TestFailToEnableEasyLoggingWhenConfigHasWrongPermissions()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Assert.Ignore("skip test on Windows");
}

// arrange
var configFilePath = CreateConfigTempFile(s_workingDirectory, Config("WARN", s_workingDirectory));
Syscall.chmod(configFilePath, FilePermissions.S_IRUSR | FilePermissions.S_IWUSR | FilePermissions.S_IWGRP);
Expand All @@ -91,19 +88,15 @@ public void TestFailToEnableEasyLoggingWhenConfigHasWrongPermissions()
var thrown = Assert.Throws<SnowflakeDbException>(() => conn.Open());

// assert
Assert.That(thrown.Message, Does.Contain("Connection string is invalid: Unable to initialize session"));
Assert.That(thrown.Message, Does.Contain("Attempting to read a file with too broad permissions assigned"));
Assert.IsFalse(EasyLoggerManager.HasEasyLoggingAppender());
}
}

[Test]
[Platform(Exclude="Win", Reason="skip test on Windows")]
public void TestFailToEnableEasyLoggingWhenLogDirectoryNotAccessible()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Assert.Ignore("skip test on Windows");
}

// arrange
var configFilePath = CreateConfigTempFile(s_workingDirectory, Config("WARN", "/"));
using (IDbConnection conn = new SnowflakeDbConnection())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,23 @@ public class EasyLoggingConfigFinderTest
public void Setup()
{
t_fileOperations = new Mock<FileOperations>();
t_unixOperations = new Mock<UnixOperations>();
t_environmentOperations = new Mock<EnvironmentOperations>();
t_finder = new EasyLoggingConfigFinder(t_fileOperations.Object, t_unixOperations.Object, t_environmentOperations.Object);
t_finder = new EasyLoggingConfigFinder(t_fileOperations.Object, t_environmentOperations.Object);
MockHomeDirectory();
MockExecutionDirectory();
}

[Test]
public void TestThatTakesFilePathFromTheInput()
{
// arrange
MockFileFromEnvironmentalVariable();
MockFileOnDriverPath();
MockFileOnHomePath();

// act
var filePath = t_finder.FindConfigFilePath(InputConfigFilePath);

// assert
Assert.AreEqual(InputConfigFilePath, filePath);
t_fileOperations.VerifyNoOtherCalls();
Expand All @@ -71,14 +70,14 @@ public void TestThatTakesFilePathFromEnvironmentVariableIfInputNotPresent(
MockFileFromEnvironmentalVariable();
MockFileOnDriverPath();
MockFileOnHomePath();

// act
var filePath = t_finder.FindConfigFilePath(inputFilePath);

// assert
Assert.AreEqual(EnvironmentalConfigFilePath, filePath);
}

[Test]
public void TestThatTakesFilePathFromDriverLocationWhenNoInputParameterNorEnvironmentVariable()
{
Expand All @@ -88,20 +87,20 @@ public void TestThatTakesFilePathFromDriverLocationWhenNoInputParameterNorEnviro

// act
var filePath = t_finder.FindConfigFilePath(null);

// assert
Assert.AreEqual(s_driverConfigFilePath, filePath);
}

[Test]
public void TestThatTakesFilePathFromHomeLocationWhenNoInputParamEnvironmentVarNorDriverLocation()
{
// arrange
MockFileOnHomePath();

// act
var filePath = t_finder.FindConfigFilePath(null);

// assert
Assert.AreEqual(s_homeConfigFilePath, filePath);
}
Expand All @@ -119,32 +118,12 @@ public void TestThatTakesFilePathFromHomeDirectoryWhenNoOtherWaysPossible()
Assert.AreEqual(s_homeConfigFilePath, filePath);
}

[Test]
public void TestThatConfigFileIsNotUsedIfOthersCanModifyTheConfigFile()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Assert.Ignore("skip test on Windows");
}

// arrange
MockFileOnHomePath();
MockHasFlagReturnsTrue();

// act
var thrown = Assert.Throws<Exception>(() => t_finder.FindConfigFilePath(null));

// assert
Assert.IsNotNull(thrown);
Assert.AreEqual(thrown.Message, $"Error due to other users having permission to modify the config file: {s_homeConfigFilePath}");
}

[Test]
public void TestThatReturnsNullIfNoWayOfGettingTheFile()
{
// act
var filePath = t_finder.FindConfigFilePath(null);

// assert
Assert.IsNull(filePath);
}
Expand All @@ -157,7 +136,7 @@ public void TestThatDoesNotFailWhenSearchForOneOfDirectoriesFails()

// act
var filePath = t_finder.FindConfigFilePath(null);

// assert
Assert.IsNull(filePath);
t_environmentOperations.Verify(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile), Times.Once);
Expand Down Expand Up @@ -186,7 +165,7 @@ public void TestThatDoesNotFailWhenHomeDirectoryDoesNotExist()

// act
var filePath = t_finder.FindConfigFilePath(null);

// assert
Assert.IsNull(filePath);
t_environmentOperations.Verify(e => e.GetFolderPath(Environment.SpecialFolder.UserProfile), Times.Once);
Expand Down Expand Up @@ -220,7 +199,7 @@ private static void MockExecutionDirectory()
.Setup(e => e.GetExecutionDirectory())
.Returns(DriverDirectory);
}

private static void MockFileOnHomePathDoesNotExist()
{
t_fileOperations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static void AfterAll()
{
Directory.Delete(s_workingDirectory, true);
}

[Test]
public void TestThatParsesConfigFile()
{
Expand All @@ -59,12 +59,12 @@ public void TestThatParsesConfigFileWithNullValues(string filePath)

// act
var config = parser.Parse(filePath);

// assert
Assert.IsNotNull(config);
Assert.IsNotNull(config.CommonProps);
Assert.IsNull(config.CommonProps.LogLevel);
Assert.IsNull(config.CommonProps.LogPath);
Assert.IsNull(config.CommonProps.LogPath);
}

[Test]
Expand All @@ -77,23 +77,23 @@ public void TestThatReturnsNullWhenNothingToParse(string noFilePath)

// act
var config = parser.Parse(noFilePath);

// assert
Assert.IsNull(config);
}

[Test]
public void TestThatFailsWhenTheFileDoesNotExist()
{
// arrange
var parser = new EasyLoggingConfigParser();

// act
var thrown = Assert.Throws<Exception>(() => parser.Parse(NotExistingFilePath));
var thrown = Assert.Throws<FileNotFoundException>(() => parser.Parse(NotExistingFilePath));

// assert
Assert.IsNotNull(thrown);
Assert.AreEqual("Finding easy logging configuration failed", thrown.Message);
Assert.AreEqual("No such file or directory", thrown.Message);
}

[Test, TestCaseSource(nameof(WrongConfigFiles))]
Expand Down
32 changes: 6 additions & 26 deletions Snowflake.Data/Configuration/EasyLoggingConfigFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ internal class EasyLoggingConfigFinder
internal const string ClientConfigEnvironmentName = "SF_CLIENT_CONFIG_FILE";

private readonly FileOperations _fileOperations;
private readonly UnixOperations _unixOperations;
private readonly EnvironmentOperations _environmentOperations;

public static readonly EasyLoggingConfigFinder Instance = new EasyLoggingConfigFinder(FileOperations.Instance, UnixOperations.Instance, EnvironmentOperations.Instance);
public static readonly EasyLoggingConfigFinder Instance = new EasyLoggingConfigFinder(FileOperations.Instance, EnvironmentOperations.Instance);

internal EasyLoggingConfigFinder(FileOperations fileOperations, UnixOperations unixFileOperations, EnvironmentOperations environmentOperations)
internal EasyLoggingConfigFinder(FileOperations fileOperations, EnvironmentOperations environmentOperations)
{
_fileOperations = fileOperations;
_unixOperations = unixFileOperations;
_environmentOperations = environmentOperations;
}

Expand All @@ -38,16 +36,12 @@ internal EasyLoggingConfigFinder()
public virtual string FindConfigFilePath(string configFilePathFromConnectionString)
{
var configFilePath = GetFilePathFromInputParameter(configFilePathFromConnectionString, "connection string")
?? GetFilePathEnvironmentVariable()
?? GetFilePathFromDriverLocation()
?? GetFilePathFromHomeDirectory();
if (configFilePath != null)
{
CheckIfValidPermissions(configFilePath);
}
?? GetFilePathEnvironmentVariable()
?? GetFilePathFromDriverLocation()
?? GetFilePathFromHomeDirectory();
return configFilePath;
}

private string GetFilePathEnvironmentVariable()
{
var filePath = _environmentOperations.GetEnvironmentVariable(ClientConfigEnvironmentName);
Expand Down Expand Up @@ -100,19 +94,5 @@ private string OnlyIfFileExists(string filePath, string directoryDescription)
}
return null;
}

private void CheckIfValidPermissions(string filePath)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return;

// Check if others have permissions to modify the file and fail if so
if (_unixOperations.CheckFileHasAnyOfPermissions(filePath, FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite))
{
var errorMessage = $"Error due to other users having permission to modify the config file: {filePath}";
s_logger.Error(errorMessage);
throw new Exception(errorMessage);
}
}
}
}
57 changes: 52 additions & 5 deletions Snowflake.Data/Configuration/EasyLoggingConfigParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Mono.Unix;
using Mono.Unix.Native;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Snowflake.Data.Client;
using Snowflake.Data.Core;
using Snowflake.Data.Core.Tools;
using Snowflake.Data.Log;

namespace Snowflake.Data.Configuration
Expand All @@ -22,30 +31,68 @@
public virtual ClientConfig Parse(string filePath)
{
var configFile = TryToReadFile(filePath);
return configFile == null ? null : TryToParseFile(configFile);
return configFile.IsNullOrEmpty() ? null : TryToParseFile(configFile);
}

/// <summary>
/// ReadAllText function reads contents of a file at the <paramref name="filePath"/> making sure, in a way not prone to race-conditions, that:
/// - A file is owned by the same user as effective user of the current process.
/// - A file is owned by the same group as effective group of the current process.
/// - A file permissions do not include `forbiddenPermissions` (any others' permissions by default)
///
/// </summary>
/// <param name="filePath">The file path of the configuration file</param>
/// <returns></returns>
/// <exception cref="SecurityException">An exception will be thrown if the file is no owned by the user or group</exception>
private string TryToReadFile(string filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return null;
return String.Empty;
}

try
{
return File.ReadAllText(filePath);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var streamReader = new StreamReader(filePath, Encoding.Default);
return streamReader.ReadToEnd();

Check warning on line 59 in Snowflake.Data/Configuration/EasyLoggingConfigParser.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Configuration/EasyLoggingConfigParser.cs#L57-L59

Added lines #L57 - L59 were not covered by tests
}
else
{
var handle = VerifyUnixPermissions(filePath);

var streamReader = new StreamReader(handle, Encoding.Default);
return streamReader.ReadToEnd();
}
}
catch (Exception e)
{
var errorMessage = "Finding easy logging configuration failed";
s_logger.Error(errorMessage, e);
throw new Exception(errorMessage);
throw;
}
}

private static UnixStream VerifyUnixPermissions(string filePath)
{
FileAccessPermissions forbiddenPermissions = FileAccessPermissions.OtherWrite | FileAccessPermissions.GroupWrite;
var fileInfo = new UnixFileInfo(path: filePath);

var handle = fileInfo.OpenRead();
if (handle.OwnerUser.UserId != Syscall.geteuid())
throw new SnowflakeDbException(SFError.INTERNAL_ERROR, "Attempting to read a file not owned by the effective user of the current process");

Check warning on line 84 in Snowflake.Data/Configuration/EasyLoggingConfigParser.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Configuration/EasyLoggingConfigParser.cs#L84

Added line #L84 was not covered by tests
if (handle.OwnerGroup.GroupId != Syscall.getegid())
throw new SnowflakeDbException(SFError.INTERNAL_ERROR, "Attempting to read a file not owned by the effective group of the current process");

Check warning on line 86 in Snowflake.Data/Configuration/EasyLoggingConfigParser.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Configuration/EasyLoggingConfigParser.cs#L86

Added line #L86 was not covered by tests
if ((handle.FileAccessPermissions & forbiddenPermissions) != 0)
throw new SnowflakeDbException(SFError.INTERNAL_ERROR, "Attempting to read a file with too broad permissions assigned");
return handle;
}

private ClientConfig TryToParseFile(string fileContent)
{
try {
try
{
var config = JsonConvert.DeserializeObject<ClientConfig>(fileContent);
Validate(config);
CheckForUnknownFields(fileContent);
Expand Down
2 changes: 1 addition & 1 deletion Snowflake.Data/Snowflake.Data.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<Authors>Snowflake</Authors>
<Version>4.0.0</Version>
<DebugType>Full</DebugType>
<LangVersion>7.3</LangVersion>
<LangVersion>8.0</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading