Как предотвратить сброс подключения выдающим себя участником?

#c# #entity-framework-core #impersonation

Вопрос:

Я создаю приложение для подключения к базе данных, которое использует безопасность на уровне строк.

Я подключаюсь с помощью EF Core и в ходе исследований обнаружил, что до тех пор, пока пользователь, запускающий приложение на сервере, имеет разрешение выдавать себя за пользователя, он выполняет запросы от имени этого пользователя Execute as login = [user login] .

У меня есть метод, который я вызываю после того, как моя фабрика контекста базы данных инициирует новый контекст.

 public static void ChangeUser(String sUser, DbContext dbContext)
{
    System.Data.Common.DbConnection dbCon = dbContext.Database.GetDbConnection();
    if (dbCon.State != ConnectionState.Open)
        dbCon.Open();
    string userInDb;
    using (var cmd = dbCon.CreateCommand())
    {
        cmd.CommandType = CommandType.Text;
        cmd.Parameters.Add(new SqlParameter("user", sUser));
        cmd.CommandText = "SELECT SUSER_NAME()";
        userInDb = (string) cmd.ExecuteScalar();
    }
    if (userInDb.ToLower() == sUser.ToLower()) return;
    using (var cmd = dbCon.CreateCommand())
    {
        cmd.CommandType = CommandType.Text;
        cmd.Parameters.Add(new SqlParameter("user", sUser));
        cmd.CommandText = "EXECUTE AS LOGIN = @user;";
        cmd.ExecuteNonQuery();
    }
}
 

Это работает для первых нескольких вызовов, но в конечном итоге я получаю ошибку: ERROR|Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost|Unhandled exception in circuit 'n1Y3qPEm60j0xxOFc6O83IrFu3jHD7EQI0_zPnO_2RQ'. Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot continue the execution because the session is in the kill state. A severe error occurred on the current command. The results, if any, should be discarded. и, просматривая журналы на SQL-сервере, я получаю дополнительную информацию с помощью журнала: The connection has been dropped because the principal that opened it subsequently assumed a new security context, and then tried to reset the connection under its impersonated security context. This scenario is not supported. See "Impersonation Overview" in Books Online.

Как бы я правильно выдал себя за пользователя, используя ядро EF, где это не приведет к «состоянию отключения» для подключения?

Я предположил, что это произошло из Execute as -за того, что его вызывали дважды, поэтому я попытался добавить проверку, прежде чем пытаться выдать себя за пользователя.

Комментарии:

1. Вы не можете запретить олицетворение пользователя на SQL-сервере?

2. SQL server имеет ограничения на уровне строк. Я пытаюсь настроить его так, чтобы мой пользователь среднего уровня (пользователь, запускающий пул приложений) мог выдавать себя за аутентифицированного пользователя с помощью среднего уровня (веб-api интрасети).

Ответ №1:

WITH COOKIE INTO ... Форма ВЫПОЛНИТЬ КАК можно использовать, чтобы избежать ошибки «Не удается продолжить выполнение, так как сеанс находится в состоянии завершения», и рекомендуется, чтобы пользователь не открыл дыру в системе безопасности, вернувшись самостоятельно.

Официальное ВЫПОЛНЕНИЕ В ВИДЕ документов

Продолжая, вот утилиты, которые я использую, чтобы сделать это простым и предсказуемым, и некоторые примеры использования.

ЗДЕСЬ МОГУТ БЫТЬ ДРАКОНЫ: будьте особенно осторожны и вдумчивы с кодом, который перемещает пользователя через границы безопасности. Будьте особенно внимательны к user тому, откуда берется аргумент, который вы передаете в эти ExecuteAs... методы расширения. Если пользователь или злоумышленник может манипулировать этой строкой, они могут злоупотреблять этими методами расширения ExecuteAs двумя способами:

  1. Очевидно, что они могут предоставить другого пользователя с повышенными правами, и тогда любой выполняемый SQL будет иметь права более сильного пользователя, чем вы, возможно, предполагали.
  2. Они могут вводить произвольный SQL, потому что эта форма EXECUTE AS может выполняться только в контексте adhoc. Имя пользователя не может быть параметризовано.

Последнее замечание, вы можете EXECUTE AS LOGIN ... или EXECUTE AS USER ... . Эти методы расширения делают последнее.

 // Usage examples

async void Main()
{
    // These two will work because the user from the connection string can SELECT anything.
    Groups.ToList();
    PaymentRecords.ToList();

    //using (var executeAsScope = await (DbContext.Database.GetDbConnection() as SqlConnection).ExecuteAsUserAsync("morePrivilegedUser'; PRINT 'do something malicious'; REVERT; EXECUTE AS USER = 'lessPrivilegedUser"))
    //using (var executeAsScope = await (DbContext.Database.GetDbConnection() as SqlConnection).ExecuteAsUserAsync("morePrivilegedUser"))
    using (var executeAsScope = await (DbContext.Database.GetDbConnection() as SqlConnection).ExecuteAsUserAsync("lessPrivilegedUser"))
    {
        // This will succeed because "lessPrivilegedUser" can SELECT the Groups table.
        Groups.ToList();
        // This will throw an exception because "lessPrivilegedUser" does not have permission to SELECT the PaymentRecords table.
        PaymentRecords.ToList();
        // This will fail because @cookie does not exist.
        Database.ExecuteSqlRaw("REVERT WITH COOKIE = @cookie;");
    }

    // This would succeed because we've exited the "lessPrivilegedUser" execute as scope and we are back to the user determined by the connection string.
    PaymentRecords.ToList();

    // Similarly you can work with transactions using the other extension method
    using (var sqlTransaction = await DbContext.Database.GetDbConnection().BeginTransactionAsync() as SqlTransaction)
    using (var dbContextTransaction = await DbContext.Database.UseTransactionAsync(sqlTransaction))
    using (var executeAsScope = await sqlTransaction.ExecuteAsUserAsync("lessPrivilegedUser"))
    {
        // This will succeed because "lessPrivilegedUser" can SELECT the Groups table.
        Groups.ToList();
        // This will fail because "lessPrivilegedUser" does not have permission to SELECT the PaymentRecords table.
        PaymentRecords.ToList();
        // This will fail because @cookie does not exist.
        Database.ExecuteSqlRaw("REVERT WITH COOKIE = @cookie;");
    }
}

// Utilities

public static class ExecuteAsExtensions
{
    public static async Task<IDisposable> ExecuteAsUserAsync(this SqlConnection connection, string user)
    {
        byte[] cookie;
        using (var cmd = connection.CreateCommand())
        {
            cmd.CommandText = $@"DECLARE @cookie VARBINARY(100); EXECUTE AS USER = '{user}' WITH COOKIE INTO @cookie; SELECT @cookie; SET @cookie = 0;";
            await connection.OpenAsync();
            cookie = (byte[])await cmd.ExecuteScalarAsync();
        }

        return new ExecuteAsScope(cookie, connection, transaction: null);
    }

    public static async Task<IDisposable> ExecuteAsUserAsync(this SqlTransaction transaction, string user)
    {
        byte[] cookie;
        using (var cmd = transaction.Connection.CreateCommand())
        {
            cmd.Transaction = transaction;
            cmd.CommandText = $@"DECLARE @cookie VARBINARY(100); EXECUTE AS USER = '{user}'  WITH COOKIE INTO @cookie; SELECT @cookie; SET @cookie = 0;";
            cookie = (byte[])await cmd.ExecuteScalarAsync();
        }

        return new ExecuteAsScope(cookie, transaction.Connection, transaction);
    }
}

public class ExecuteAsScope : IDisposable
{

    private byte[] cookie;
    private SqlConnection connection;
    private SqlTransaction transaction;

    public ExecuteAsScope(byte[] cookie, SqlConnection connection, SqlTransaction transaction)
    {
        this.cookie = cookie;
        this.connection = connection;
        this.transaction = transaction;
    }

    #region IDisposable Support
    private bool disposedValue = false;
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                using (var cmd = connection.CreateCommand())
                {
                    cmd.Transaction = transaction;
                    cmd.CommandText = $@"REVERT WITH COOKIE = @cookie";
                    cmd.Parameters.AddWithValue("@cookie", cookie);
                    cmd.ExecuteNonQuery();
                }
                transaction = null;
                connection = null;
            }

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
    #endregion
}