CVE-2024-32655: Insufficient Information (NVD-CWE-noinfo)
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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-14T15:36Z
Repository: https://github.com/npgsql/npgsql
Language: C#
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 date: 2024-05-09 14:20:06 +0300
Commit message: Merge pull request from GHSA-x9vc-6hfv-hg8c


Diff: diff --git a/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs b/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs
index f3e31731..9e0fd45d 100644
--- a/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs
+++ b/src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs
@@ -17,10 +17,11 @@ internal Task WriteDescribe(StatementOrPortal statementOrPortal, byte[] asciiNam
                   sizeof(int)  +       // Length
                   sizeof(byte) +       // Statement or portal
                   (asciiName.Length + 1);   // Statement/portal name
 
         var writeBuffer = WriteBuffer;
+        writeBuffer.StartMessage(len);
         if (writeBuffer.WriteSpaceLeft < len)
             return FlushAndWrite(len, statementOrPortal, asciiName, async, cancellationToken);
 
         Write(writeBuffer, len, statementOrPortal, asciiName);
         return Task.CompletedTask;
@@ -46,10 +47,11 @@ internal Task WriteSync(bool async, CancellationToken cancellationToken = defaul
     {
         const int len = sizeof(byte) +  // Message code
                         sizeof(int);    // Length
 
         var writeBuffer = WriteBuffer;
+        writeBuffer.StartMessage(len);
         if (writeBuffer.WriteSpaceLeft < len)
             return FlushAndWrite(async, cancellationToken);
 
         Write(writeBuffer);
         return Task.CompletedTask;
@@ -77,10 +79,11 @@ internal Task WriteExecute(int maxRows, bool async, CancellationToken cancellati
                         sizeof(int)  +       // Length
                         sizeof(byte) +       // Null-terminated portal name (always empty for now)
                         sizeof(int);         // Max number of rows
 
         var writeBuffer = WriteBuffer;
+        writeBuffer.StartMessage(len);
         if (writeBuffer.WriteSpaceLeft < len)
             return FlushAndWrite(maxRows, async, cancellationToken);
 
         Write(writeBuffer, maxRows);
         return Task.CompletedTask;
@@ -116,25 +119,27 @@ internal async Task WriteParse(string sql, byte[] asciiName, List<NpgsqlParamete
             Break(e);
             throw;
         }
 
         var writeBuffer = WriteBuffer;
-        if (writeBuffer.WriteSpaceLeft < 1 + 4 + asciiName.Length + 1)
-            await Flush(async, cancellationToken).ConfigureAwait(false);
-
         var messageLength =
             sizeof(byte)                +         // Message code
             sizeof(int)                 +         // Length
             asciiName.Length        +         // Statement name
             sizeof(byte)                +         // Null terminator for the statement name
             queryByteLen + sizeof(byte) +         // SQL query length plus null terminator
             sizeof(ushort)              +         // Number of parameters
             inputParameters.Count * sizeof(int);  // Parameter OIDs
 
-        writeBuffer.WriteByte(FrontendMessageCode.Parse);
-        writeBuffer.WriteInt32(messageLength - 1);
-        writeBuffer.WriteNullTerminatedString(asciiName);
+
+        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);
 
         await writeBuffer.WriteString(sql, queryByteLen, async, cancellationToken).ConfigureAwait(false);
 
         if (writeBuffer.WriteSpaceLeft < 1 + 2)
             await Flush(async, cancellationToken).ConfigureAwait(false);
@@ -169,16 +174,10 @@ internal async Task WriteParse(string sql, byte[] asciiName, List<NpgsqlParamete
             sizeof(byte)                    +     // Portal is always empty (only a null terminator)
             asciiName.Length + sizeof(byte) +     // Statement name plus null terminator
             sizeof(ushort);                       // Number of parameter format codes that follow
 
         var writeBuffer = WriteBuffer;
-        if (writeBuffer.WriteSpaceLeft < headerLength)
-        {
-            Debug.Assert(writeBuffer.Size >= headerLength, "Write buffer too small for Bind header");
-            await Flush(async, cancellationToken).ConfigureAwait(false);
-        }
-
         var formatCodesSum = 0;
         var paramsLength = 0;
         for (var paramIndex = 0; paramIndex < parameters.Count; paramIndex++)
         {
             var param = parameters[paramIndex];
@@ -195,12 +194,19 @@ internal async Task WriteParse(string sql, byte[] asciiName, List<NpgsqlParamete
                             sizeof(int) * parameters.Count  +                       // Parameter lengths
                             paramsLength                         +                  // Parameter values
                             sizeof(short)                        +                  // Number of result format codes
                             sizeof(short) * (unknownResultTypeList?.Length ?? 1);   // Result format codes
 
-        writeBuffer.WriteByte(FrontendMessageCode.Bind);
-        writeBuffer.WriteInt32(messageLength - 1);
+        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);
         Debug.Assert(portal == string.Empty);
         writeBuffer.WriteByte(0);  // Portal is always empty
 
         writeBuffer.WriteNullTerminatedString(asciiName);
         writeBuffer.WriteInt16((short)formatCodeListLength);
@@ -267,10 +273,11 @@ internal Task WriteClose(StatementOrPortal type, byte[] asciiName, bool async, C
                   sizeof(int)  +               // Length
                   sizeof(byte) +               // Statement or portal
                   asciiName.Length + sizeof(byte);  // Statement or portal name plus null terminator
 
         var writeBuffer = WriteBuffer;
+        writeBuffer.StartMessage(len);
         if (writeBuffer.WriteSpaceLeft < len)
             return FlushAndWrite(len, type, asciiName, async, cancellationToken);
 
         Write(writeBuffer, len, type, asciiName);
         return Task.CompletedTask;
@@ -294,18 +301,21 @@ static void Write(NpgsqlWriteBuffer writeBuffer, int len, StatementOrPortal type
 
     internal async Task WriteQuery(string sql, bool async, CancellationToken cancellationToken = default)
     {
         var queryByteLen = TextEncoding.GetByteCount(sql);
 
+        var len = sizeof(byte) +
+                  sizeof(int) + // Message length (including self excluding code)
+                  queryByteLen + // Query byte length
+                  sizeof(byte);
+
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < 1 + 4)
             await Flush(async, cancellationToken).ConfigureAwait(false);
 
         WriteBuffer.WriteByte(FrontendMessageCode.Query);
-        WriteBuffer.WriteInt32(
-            sizeof(int)  +        // Message length (including self excluding code)
-            queryByteLen +        // Query byte length
-            sizeof(byte));        // Null terminator
+        WriteBuffer.WriteInt32(len - 1);
 
         await WriteBuffer.WriteString(sql, queryByteLen, async, cancellationToken).ConfigureAwait(false);
         if (WriteBuffer.WriteSpaceLeft < 1)
             await Flush(async, cancellationToken).ConfigureAwait(false);
         WriteBuffer.WriteByte(0);  // Null terminator
@@ -314,10 +324,11 @@ internal async Task WriteQuery(string sql, bool async, CancellationToken cancell
     internal async Task WriteCopyDone(bool async, CancellationToken cancellationToken = default)
     {
         const int len = sizeof(byte) +   // Message code
                         sizeof(int);     // Length
 
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < len)
             await Flush(async, cancellationToken).ConfigureAwait(false);
 
         WriteBuffer.WriteByte(FrontendMessageCode.CopyDone);
         WriteBuffer.WriteInt32(len - 1);
@@ -329,10 +340,11 @@ internal async Task WriteCopyFail(bool async, CancellationToken cancellationToke
 
         const int len = sizeof(byte) +  // Message code
                         sizeof(int) +   // Length
                         sizeof(byte);   // Error message is always empty (only a null terminator)
 
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < len)
             await Flush(async, cancellationToken).ConfigureAwait(false);
 
         WriteBuffer.WriteByte(FrontendMessageCode.CopyFail);
         WriteBuffer.WriteInt32(len - 1);
@@ -346,10 +358,11 @@ internal void WriteCancelRequest(int backendProcessId, int backendSecretKey)
                         sizeof(int) +  // Backend process id
                         sizeof(int);   // Backend secret key
 
         Debug.Assert(backendProcessId != 0);
 
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < len)
             Flush(false).GetAwaiter().GetResult();
 
         WriteBuffer.WriteInt32(len);
         WriteBuffer.WriteInt32(1234 << 16 | 5678);
@@ -360,10 +373,11 @@ internal void WriteCancelRequest(int backendProcessId, int backendSecretKey)
     internal void WriteTerminate()
     {
         const int len = sizeof(byte) +  // Message code
                         sizeof(int);    // Length
 
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < len)
             Flush(false).GetAwaiter().GetResult();
 
         WriteBuffer.WriteByte(FrontendMessageCode.Terminate);
         WriteBuffer.WriteInt32(len - 1);
@@ -372,10 +386,11 @@ internal void WriteTerminate()
     internal void WriteSslRequest()
     {
         const int len = sizeof(int) +  // Length
                         sizeof(int);   // SSL request code
 
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < len)
             Flush(false).GetAwaiter().GetResult();
 
         WriteBuffer.WriteInt32(len);
         WriteBuffer.WriteInt32(80877103);
@@ -392,10 +407,11 @@ internal void WriteStartup(Dictionary<string, string> parameters)
         foreach (var kvp in parameters)
             len += NpgsqlWriteBuffer.UTF8Encoding.GetByteCount(kvp.Key) + 1 +
                    NpgsqlWriteBuffer.UTF8Encoding.GetByteCount(kvp.Value) + 1;
 
         // Should really never happen, just in case
+        WriteBuffer.StartMessage(len);
         if (len > WriteBuffer.Size)
             throw new Exception("Startup message bigger than buffer");
 
         WriteBuffer.WriteInt32(len);
         WriteBuffer.WriteInt32(protocolVersion3);
@@ -415,12 +431,14 @@ internal void WriteStartup(Dictionary<string, string> parameters)
 
     internal Task WritePassword(byte[] payload, bool async, CancellationToken cancellationToken = default) => WritePassword(payload, 0, payload.Length, async, cancellationToken);
 
     internal async Task WritePassword(byte[] payload, int offset, int count, bool async, CancellationToken cancellationToken = default)
     {
+        WriteBuffer.StartMessage(sizeof(byte) + sizeof(int) + count);
         if (WriteBuffer.WriteSpaceLeft < sizeof(byte) + sizeof(int))
             await WriteBuffer.Flush(async, cancellationToken).ConfigureAwait(false);
+
         WriteBuffer.WriteByte(FrontendMessageCode.Password);
         WriteBuffer.WriteInt32(sizeof(int) + count);
 
         if (count <= WriteBuffer.WriteSpaceLeft)
         {
@@ -439,10 +457,11 @@ internal async Task WriteSASLInitialResponse(string mechanism, byte[] initialRes
                   sizeof(int)                                                +  // Length
                   NpgsqlWriteBuffer.UTF8Encoding.GetByteCount(mechanism) + sizeof(byte) +  // Mechanism plus null terminator
                   sizeof(int)                                                +  // Initial response length
                   (initialResponse?.Length ?? 0);                               // Initial response payload
 
+        WriteBuffer.StartMessage(len);
         if (WriteBuffer.WriteSpaceLeft < len)
             await WriteBuffer.Flush(async, cancellationToken).ConfigureAwait(false);
 
         WriteBuffer.WriteByte(FrontendMessageCode.Password);
         WriteBuffer.WriteInt32(len - 1);
@@ -462,10 +481,11 @@ internal async Task WriteSASLInitialResponse(string mechanism, byte[] initialRes
 
     #endregion Authentication
 
     internal Task WritePregenerated(byte[] data, bool async = false, CancellationToken cancellationToken = default)
     {
+        WriteBuffer.StartMessage(data.Length);
         if (WriteBuffer.WriteSpaceLeft < data.Length)
             return FlushAndWrite(data, async, cancellationToken);
 
         WriteBuffer.WriteBytes(data, 0, data.Length);
         return Task.CompletedTask;
diff --git a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs
index 06b49d9c..821bb7e6 100644
--- a/src/Npgsql/Internal/NpgsqlWriteBuffer.cs
+++ b/src/Npgsql/Internal/NpgsqlWriteBuffer.cs
@@ -26,10 +26,12 @@ sealed class NpgsqlWriteBuffer : IDisposable
     internal readonly NpgsqlConnector Connector;
 
     internal Stream Underlying { private get; set; }
 
     readonly Socket? _underlyingSocket;
+    internal bool MessageLengthValidation { get; set; } = true;
+
     readonly ResettableCancellationTokenSource _timeoutCts;
     readonly MetricsReporter? _metricsReporter;
 
     /// <summary>
     /// Timeout for sync and async writes
@@ -74,10 +76,13 @@ internal PgWriter GetWriter(NpgsqlDatabaseInfo typeCatalog, FlushMode flushMode
     internal readonly byte[] Buffer;
     readonly Encoder _textEncoder;
 
     internal int WritePosition;
 
+    int _messageBytesFlushed;
+    int? _messageLength;
+
     bool _disposed;
     readonly PgWriter _pgWriter;
 
     /// <summary>
     /// The minimum buffer size possible.
@@ -129,10 +134,12 @@ public async Task Flush(bool async, CancellationToken cancellationToken = defaul
             WritePosition = 1;
             WriteInt32(pos - 1);
             WritePosition = pos;
         } else if (WritePosition == 0)
             return;
+        else
+            AdvanceMessageBytesFlushed(WritePosition);
 
         var finalCt = async && Timeout > TimeSpan.Zero
             ? _timeoutCts.Start(cancellationToken)
             : cancellationToken;
 
@@ -195,19 +202,23 @@ internal void DirectWrite(ReadOnlySpan<byte> buffer)
             // Flush has already written the CopyData header for us, but write the CopyData
             // header to the socket with the write length before we can start writing the data directly.
             Debug.Assert(WritePosition == 5);
 
             WritePosition = 1;
-            WriteInt32(buffer.Length + 4);
+            WriteInt32(checked(buffer.Length + 4));
             WritePosition = 5;
             _copyMode = false;
+            StartMessage(5);
             Flush();
             _copyMode = true;
             WriteCopyDataHeader();  // And ready the buffer after the direct write completes
         }
         else
+        {
             Debug.Assert(WritePosition == 0);
+            AdvanceMessageBytesFlushed(buffer.Length);
+        }
 
         try
         {
             Underlying.Write(buffer);
         }
@@ -226,19 +237,23 @@ internal async Task DirectWrite(ReadOnlyMemory<byte> memory, bool async, Cancell
             // Flush has already written the CopyData header for us, but write the CopyData
             // header to the socket with the write length before we can start writing the data directly.
             Debug.Assert(WritePosition == 5);
 
             WritePosition = 1;
-            WriteInt32(memory.Length + 4);
+            WriteInt32(checked(memory.Length + 4));
             WritePosition = 5;
             _copyMode = false;
+            StartMessage(5);
             await Flush(async, cancellationToken).ConfigureAwait(false);
             _copyMode = true;
             WriteCopyDataHeader();  // And ready the buffer after the direct write completes
         }
         else
+        {
             Debug.Assert(WritePosition == 0);
+            AdvanceMessageBytesFlushed(memory.Length);
+        }
 
         try
         {
             if (async)
                 await Underlying.WriteAsync(memory, cancellationToken).ConfigureAwait(false);
@@ -532,13 +547,55 @@ public void Dispose()
 
     #endregion
 
     #region Misc
 
+    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"));
+        }
+    }
+
     internal void Clear()
     {
         WritePosition = 0;
+        _messageLength = null;
     }
 
     /// <summary>
     /// Returns all contents currently written to the buffer (but not flushed).
     /// Useful for pre-generating messages.
diff --git a/src/Npgsql/NpgsqlTransaction.cs b/src/Npgsql/NpgsqlTransaction.cs
index 6481e185..7819af47 100644
--- a/src/Npgsql/NpgsqlTransaction.cs
+++ b/src/Npgsql/NpgsqlTransaction.cs
@@ -210,20 +210,11 @@ public override void Save(string name)
         if (RequiresQuoting(name))
             name = $"\"{name.Replace("\"", "\"\"")}\"";
 
         // Note: savepoint names are PostgreSQL identifiers, and so limited by default to 63 characters.
         // Since we are prepending, we assume below that the statement will always fit in the buffer.
-        _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);
+        _connector.WriteQuery("SAVEPOINT " + name, async: false).GetAwaiter().GetResult();
 
         _connector.PendingPrependedResponses += 2;
     }
 
     /// <summary>
diff --git a/test/Npgsql.Tests/CommandTests.cs b/test/Npgsql.Tests/CommandTests.cs
index 9c42da9c..51e55885 100644
--- a/test/Npgsql.Tests/CommandTests.cs
+++ b/test/Npgsql.Tests/CommandTests.cs
@@ -850,10 +850,180 @@ public async Task Use_after_reload_types_invalidates_cached_infos()
             await reader.ReadAsync();
             Assert.DoesNotThrow(() => reader.GetInt32(0));
         }
     }
 
+    [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());
+    }
+
     [Test, Description("CreateCommand before connection open")]
     [IssueLink("https://github.com/npgsql/npgsql/issues/565")]
     public async Task Create_command_before_connection_open()
     {
         using var conn = new NpgsqlConnection(ConnectionString);
@@ -1025,10 +1195,11 @@ public async Task Too_many_parameters_throws([Values(PrepareOrNot.NotPrepared, P
             if (i > 0)
                 sb.Append(", ");
             sb.Append('@');
             sb.Append(paramName);
         }
+
         cmd.CommandText = sb.ToString();
 
         if (prepare == PrepareOrNot.Prepared)
         {
             Assert.That(() => cmd.Prepare(), Throws.Exception
diff --git a/test/Npgsql.Tests/Support/PgPostmasterMock.cs b/test/Npgsql.Tests/Support/PgPostmasterMock.cs
index e45c1a7f..ab3eeab5 100644
--- a/test/Npgsql.Tests/Support/PgPostmasterMock.cs
+++ b/test/Npgsql.Tests/Support/PgPostmasterMock.cs
@@ -136,10 +136,11 @@ async Task<ServerOrCancellationRequest> Accept(bool completeCancellationImmediat
 
         var stream = new NetworkStream(clientSocket, true);
         var readBuffer = new NpgsqlReadBuffer(null!, stream, clientSocket, ReadBufferSize, Encoding,
             RelaxedEncoding);
         var writeBuffer = new NpgsqlWriteBuffer(null!, stream, clientSocket, WriteBufferSize, Encoding);
+        writeBuffer.MessageLengthValidation = false;
 
         await readBuffer.EnsureAsync(4);
         var len = readBuffer.ReadInt32();
         await readBuffer.EnsureAsync(len - 4);
 
diff --git a/test/Npgsql.Tests/Support/PgServerMock.cs b/test/Npgsql.Tests/Support/PgServerMock.cs
index c34a9315..9f7a7996 100644
--- a/test/Npgsql.Tests/Support/PgServerMock.cs
+++ b/test/Npgsql.Tests/Support/PgServerMock.cs
@@ -39,10 +39,11 @@ class PgServerMock : IDisposable
     {
         ProcessId = processId;
         _stream = stream;
         _readBuffer = readBuffer;
         _writeBuffer = writeBuffer;
+        writeBuffer.MessageLengthValidation = false;
     }
 
     internal async Task Startup(MockState state)
     {
         // Read and skip the startup message
diff --git a/test/Npgsql.Tests/WriteBufferTests.cs b/test/Npgsql.Tests/WriteBufferTests.cs
index 99e5626b..ff8d9413 100644
--- a/test/Npgsql.Tests/WriteBufferTests.cs
+++ b/test/Npgsql.Tests/WriteBufferTests.cs
@@ -110,10 +110,11 @@ public void Chunked_char_array_encoding_fits_with_surrogates()
     [SetUp]
     public void SetUp()
     {
         Underlying = new MemoryStream();
         WriteBuffer = new NpgsqlWriteBuffer(null, Underlying, null, NpgsqlReadBuffer.DefaultSize, NpgsqlWriteBuffer.UTF8Encoding);
+        WriteBuffer.MessageLengthValidation = false;
     }
 
     // ReSharper disable once InconsistentNaming
     NpgsqlWriteBuffer WriteBuffer = default!;
     // ReSharper disable once InconsistentNaming

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;