fix(sql-dialect): enforce Postgres/Oracle identifier ceilings in UTF-8 bytes (Driver.Sql.Dialect-001)
NAMEDATALEN-1 (63) and Oracle 12.2's 128 are byte limits in the database encoding, not character counts, so a 32-character Cyrillic name passed ident.Length and was then silently truncated (Postgres) or rejected by the server (Oracle). Both dialects now measure Encoding.UTF8.GetByteCount and say 'bytes' in the rejection. SqlServerDialect deliberately keeps ident.Length — sysname is nvarchar(128), a real character limit — pinned by a control test.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Data.Common;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Npgsql;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
||||
@@ -20,6 +21,9 @@ public sealed class PostgresDialect : ISqlDialect
|
||||
/// PostgreSQL's identifier ceiling — <c>NAMEDATALEN - 1</c>, 63 by default. Enforced because the
|
||||
/// server <b>silently truncates</b> anything longer, so a 70-character authored name would resolve to
|
||||
/// a different object than it spells; rejecting it surfaces the config mistake instead.
|
||||
/// <para><b>The unit is BYTES in the database encoding, not characters</b> — see
|
||||
/// <see cref="QuoteIdentifier"/>. Practically that means UTF-8 on any modern estate, so a 32-character
|
||||
/// Cyrillic name is already over the limit.</para>
|
||||
/// </summary>
|
||||
internal const int MaxIdentifierLength = 63;
|
||||
|
||||
@@ -149,12 +153,19 @@ public sealed class PostgresDialect : ISqlDialect
|
||||
/// <para><b>Rejects</b> (all as <see cref="ArgumentException"/>, mirroring
|
||||
/// <c>SqlServerDialect.QuoteIdentifier</c> so a test written against one dialect fails the same way
|
||||
/// against the other): null / empty / all-whitespace; longer than
|
||||
/// <see cref="MaxIdentifierLength"/>; any control character (which covers embedded NUL and the C0/C1
|
||||
/// <see cref="MaxIdentifierLength"/> <b>bytes</b>; any control character (which covers embedded NUL and the C0/C1
|
||||
/// ranges that can truncate or corrupt a logged/audited statement); and any Unicode
|
||||
/// <see cref="UnicodeCategory.Format"/> character — bidi overrides, zero-width spaces, soft hyphen,
|
||||
/// BOM. Those last cannot escape the quotes, but they spoof the <em>rendered</em> text of a log line
|
||||
/// or an AdminUI label while comparing byte-different from the real catalog name (the Trojan-Source
|
||||
/// class, CVE-2021-42574).</para>
|
||||
/// <para><b>The length ceiling is measured in UTF-8 bytes, not characters</b> — <c>NAMEDATALEN</c>
|
||||
/// bounds the stored name in the database encoding, and every modern estate is UTF-8. Counting UTF-16
|
||||
/// code units (<c>ident.Length</c>) would accept a 32-character Cyrillic name, which is 64 bytes and
|
||||
/// is therefore truncated server-side to a name that denotes a <em>different</em> object — precisely
|
||||
/// the silent-truncation failure this rule exists to prevent. <c>SqlServerDialect</c> deliberately does
|
||||
/// <b>not</b> do this: T-SQL's <c>sysname</c> is <c>nvarchar(128)</c>, a genuine 128-character limit,
|
||||
/// so counting characters is correct there and counting bytes would reject legal names.</para>
|
||||
/// <para>NUL deserves its own note here: Postgres rejects a NUL byte anywhere in a query string
|
||||
/// outright, so passing one through would fail the whole statement rather than just the one
|
||||
/// identifier.</para>
|
||||
@@ -175,9 +186,13 @@ public sealed class PostgresDialect : ISqlDialect
|
||||
throw new ArgumentException(
|
||||
"A SQL identifier must be a non-empty, non-whitespace name.", nameof(ident));
|
||||
|
||||
if (ident.Length > MaxIdentifierLength)
|
||||
// Byte count, not ident.Length: NAMEDATALEN bounds the stored name in the database encoding, so a
|
||||
// 32-character Cyrillic name is 64 UTF-8 bytes and the server truncates it — the exact silent
|
||||
// wrong-object outcome this check exists to prevent, and one a UTF-16 code-unit count waves through.
|
||||
var byteLength = Encoding.UTF8.GetByteCount(ident);
|
||||
if (byteLength > MaxIdentifierLength)
|
||||
throw new ArgumentException(
|
||||
$"A PostgreSQL identifier may not exceed {MaxIdentifierLength} characters (got {ident.Length}).",
|
||||
$"A PostgreSQL identifier may not exceed {MaxIdentifierLength} bytes (got {byteLength}).",
|
||||
nameof(ident));
|
||||
|
||||
foreach (var ch in ident)
|
||||
|
||||
Reference in New Issue
Block a user