CVE-2024-32655 (C#, published 2024-05-14T15:36Z)
Description: value: Npgsql is the .NET data provider for PostgreSQL. The `WriteBind()` method in `src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs` uses `int` variables to store the message length and the sum of parameter lengths. Both variables overflow when the sum of parameter lengths becomes too large. This causes Npgsql to write a message size that is too small when constructing a Postgres protocol message to send it over the network to the database. When parsing the message, the database will only read a small number of bytes and treat any following bytes as new messages while they belong to the old message. Attackers can abuse this to inject arbitrary Postgres protocol messages into the connection, leading to the execution of arbitrary SQL statements on the application's behalf. This vulnerability is fixed in 4.0.14, 4.1.13, 5.0.18, 6.0.11, 7.0.7, and 8.0.3.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/npgsql/npgsql
Files changed:
src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs
src/Npgsql/Internal/NpgsqlWriteBuffer.cs
src/Npgsql/NpgsqlTransaction.cs
test/Npgsql.Tests/CommandTests.cs
test/Npgsql.Tests/Support/PgPostmasterMock.cs
test/Npgsql.Tests/Support/PgServerMock.cs
test/Npgsql.Tests/WriteBufferTests.cs
Commit message: Merge pull request from GHSA-x9vc-6hfv-hg8c



Vulnerable code:
        if (writeBuffer.WriteSpaceLeft < 1 + 4 + asciiName.Length + 1)
            await Flush(async, cancellationToken).ConfigureAwait(false);

        writeBuffer.WriteByte(FrontendMessageCode.Parse);
        writeBuffer.WriteInt32(messageLength - 1);
        writeBuffer.WriteNullTerminatedString(asciiName);
        if (writeBuffer.WriteSpaceLeft < headerLength)
        {
            Debug.Assert(writeBuffer.Size >= headerLength, "Write buffer too small for Bind header");
            await Flush(async, cancellationToken).ConfigureAwait(false);
        }

        writeBuffer.WriteByte(FrontendMessageCode.Bind);
        writeBuffer.WriteInt32(messageLength - 1);
        WriteBuffer.WriteInt32(
            sizeof(int)  +        // Message length (including self excluding code)
            queryByteLen +        // Query byte length
            sizeof(byte));        // Null terminator
            WriteInt32(buffer.Length + 4);
            WriteInt32(memory.Length + 4);
        _connector.WriteBuffer.WriteByte(FrontendMessageCode.Query);
        _connector.WriteBuffer.WriteInt32(
            sizeof(int)  +                               // Message length (including self excluding code)
            _connector.TextEncoding.GetByteCount("SAVEPOINT ") +
            _connector.TextEncoding.GetByteCount(name) +
            sizeof(byte));                               // Null terminator

        _connector.WriteBuffer.WriteString("SAVEPOINT ");
        _connector.WriteBuffer.WriteString(name);
        _connector.WriteBuffer.WriteByte(0);

Fixed code:
        writeBuffer.StartMessage(len);
        writeBuffer.StartMessage(len);
        writeBuffer.StartMessage(len);

        WriteBuffer.StartMessage(messageLength);
        if (WriteBuffer.WriteSpaceLeft < 1 + 4 + asciiName.Length + 1)
            await Flush(async, cancellationToken).ConfigureAwait(false);

        WriteBuffer.WriteByte(FrontendMessageCode.Parse);
        WriteBuffer.WriteInt32(messageLength - 1);
        WriteBuffer.WriteNullTerminatedString(asciiName);
        WriteBuffer.StartMessage(messageLength);
        if (WriteBuffer.WriteSpaceLeft < headerLength)
        {
            Debug.Assert(WriteBuffer.Size >= headerLength, "Write buffer too small for Bind header");
            await Flush(async, cancellationToken).ConfigureAwait(false);
        }

        WriteBuffer.WriteByte(FrontendMessageCode.Bind);
        WriteBuffer.WriteInt32(messageLength - 1);
        writeBuffer.StartMessage(len);
        var len = sizeof(byte) +
                  sizeof(int) + // Message length (including self excluding code)
                  queryByteLen + // Query byte length
                  sizeof(byte);

        WriteBuffer.StartMessage(len);
        WriteBuffer.WriteInt32(len - 1);
        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(sizeof(byte) + sizeof(int) + count);

        WriteBuffer.StartMessage(len);
        WriteBuffer.StartMessage(data.Length);
    internal bool MessageLengthValidation { get; set; } = true;

    int _messageBytesFlushed;
    int? _messageLength;

        else
            AdvanceMessageBytesFlushed(WritePosition);
            WriteInt32(checked(buffer.Length + 4));
            StartMessage(5);
        {
            AdvanceMessageBytesFlushed(buffer.Length);
        }
            WriteInt32(checked(memory.Length + 4));
            StartMessage(5);
        {
            AdvanceMessageBytesFlushed(memory.Length);
        }
    internal void StartMessage(int messageLength)
    {
        if (!MessageLengthValidation)
            return;

        if (_messageLength is not null && _messageBytesFlushed != _messageLength && WritePosition != -_messageBytesFlushed + _messageLength)
            Throw();

        // Add negative WritePosition to compensate for previous message(s) written without flushing.
        _messageBytesFlushed = -WritePosition;
        _messageLength = messageLength;

        void Throw()
        {
            throw Connector.Break(new OverflowException("Did not write the amount of bytes the message length specified"));
        }
    }

    void AdvanceMessageBytesFlushed(int count)
    {
        if (!MessageLengthValidation)
            return;

        if (count < 0 || _messageLength is null || (long)_messageBytesFlushed + count > _messageLength)
            Throw();

        _messageBytesFlushed += count;

        void Throw()
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count), "Can't advance by a negative count");

            if (_messageLength is null)
                throw Connector.Break(new InvalidOperationException("No message was started"));

            if ((long)_messageBytesFlushed + count > _messageLength)
                throw Connector.Break(new OverflowException("Tried to write more bytes than the message length specified"));
        }
    }

        _messageLength = null;
        _connector.WriteQuery("SAVEPOINT " + name, async: false).GetAwaiter().GetResult();
    [Test]
    public async Task Parameter_overflow_message_length_throws()
    {
        await using var conn = CreateConnection();
        await conn.OpenAsync();
        await using var cmd = new NpgsqlCommand("SELECT @a, @b, @c, @d, @e, @f, @g, @h", conn);

        var largeParam = new string('A', 1 << 29);
        cmd.Parameters.AddWithValue("a", largeParam);
        cmd.Parameters.AddWithValue("b", largeParam);
        cmd.Parameters.AddWithValue("c", largeParam);
        cmd.Parameters.AddWithValue("d", largeParam);
        cmd.Parameters.AddWithValue("e", largeParam);
        cmd.Parameters.AddWithValue("f", largeParam);
        cmd.Parameters.AddWithValue("g", largeParam);
        cmd.Parameters.AddWithValue("h", largeParam);

        Assert.ThrowsAsync<OverflowException>(() => cmd.ExecuteReaderAsync());
    }

    [Test]
    public async Task Composite_overflow_message_length_throws()
    {
        await using var adminConnection = await OpenConnectionAsync();
        var type = await GetTempTypeName(adminConnection);

        await adminConnection.ExecuteNonQueryAsync(
            $"CREATE TYPE {type} AS (a text, b text, c text, d text, e text, f text, g text, h text)");

        var dataSourceBuilder = CreateDataSourceBuilder();
        dataSourceBuilder.MapComposite<BigComposite>(type);
        await using var dataSource = dataSourceBuilder.Build();
        await using var connection = await dataSource.OpenConnectionAsync();

        var largeString = new string('A', 1 << 29);

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT @a";
        cmd.Parameters.AddWithValue("a", new BigComposite
        {
            A = largeString,
            B = largeString,
            C = largeString,
            D = largeString,
            E = largeString,
            F = largeString,
            G = largeString,
            H = largeString
        });

        Assert.ThrowsAsync<OverflowException>(async () => await cmd.ExecuteNonQueryAsync());
    }

    record BigComposite
    {
        public string A { get; set; } = null!;
        public string B { get; set; } = null!;
        public string C { get; set; } = null!;
        public string D { get; set; } = null!;
        public string E { get; set; } = null!;
        public string F { get; set; } = null!;
        public string G { get; set; } = null!;
        public string H { get; set; } = null!;
    }

    [Test]
    public async Task Array_overflow_message_length_throws()
    {
        await using var connection = await OpenConnectionAsync();

        var largeString = new string('A', 1 << 29);

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT @a";
        var array = new[]
        {
            largeString,
            largeString,
            largeString,
            largeString,
            largeString,
            largeString,
            largeString,
            largeString
        };
        cmd.Parameters.AddWithValue("a", array);

        Assert.ThrowsAsync<OverflowException>(async () => await cmd.ExecuteNonQueryAsync());
    }

    [Test]
    public async Task Range_overflow_message_length_throws()
    {
        await using var adminConnection = await OpenConnectionAsync();
        var type = await GetTempTypeName(adminConnection);
        var rangeType = await GetTempTypeName(adminConnection);

        await adminConnection.ExecuteNonQueryAsync(
            $"CREATE TYPE {type} AS (a text, b text, c text, d text, e text, f text, g text, h text);CREATE TYPE {rangeType} AS RANGE(subtype={type})");

        var dataSourceBuilder = CreateDataSourceBuilder();
        dataSourceBuilder.MapComposite<BigComposite>(type);
        dataSourceBuilder.EnableUnmappedTypes();
        await using var dataSource = dataSourceBuilder.Build();
        await using var connection = await dataSource.OpenConnectionAsync();

        var largeString = new string('A', (1 << 28) + 2000000);

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT @a";
        var composite = new BigComposite
        {
            A = largeString,
            B = largeString,
            C = largeString,
            D = largeString
        };
        var range = new NpgsqlRange<BigComposite>(composite, composite);
        cmd.Parameters.Add(new NpgsqlParameter
        {
            Value = range,
            ParameterName = "a",
            DataTypeName = rangeType
        });

        Assert.ThrowsAsync<OverflowException>(async () => await cmd.ExecuteNonQueryAsync());
    }

    [Test]
    public async Task Multirange_overflow_message_length_throws()
    {
        await using var adminConnection = await OpenConnectionAsync();
        var type = await GetTempTypeName(adminConnection);
        var rangeType = await GetTempTypeName(adminConnection);

        await adminConnection.ExecuteNonQueryAsync(
            $"CREATE TYPE {type} AS (a text, b text, c text, d text, e text, f text, g text, h text);CREATE TYPE {rangeType} AS RANGE(subtype={type})");

        var dataSourceBuilder = CreateDataSourceBuilder();
        dataSourceBuilder.MapComposite<BigComposite>(type);
        dataSourceBuilder.EnableUnmappedTypes();
        await using var dataSource = dataSourceBuilder.Build();
        await using var connection = await dataSource.OpenConnectionAsync();

        var largeString = new string('A', (1 << 28) + 2000000);

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = "SELECT @a";
        var composite = new BigComposite
        {
            A = largeString
        };
        var range = new NpgsqlRange<BigComposite>(composite, composite);
        var multirange = new[]
        {
            range,
            range,
            range,
            range
        };
        cmd.Parameters.Add(new NpgsqlParameter
        {
            Value = multirange,
            ParameterName = "a",
            DataTypeName = rangeType + "_multirange"
        });

        Assert.ThrowsAsync<OverflowException>(async () => await cmd.ExecuteNonQueryAsync());
    }


        writeBuffer.MessageLengthValidation = false;
        writeBuffer.MessageLengthValidation = false;
        WriteBuffer.MessageLengthValidation = false;