Three findings from the code review of the dialect seam:
- QuoteIdentifier used char.IsControl, which only covers Unicode category Cc.
Bidi overrides, zero-width spaces, soft hyphen and BOM are category Cf and
passed through untouched. They cannot escape the brackets, so this is not an
injection bypass — but the method rejects control characters precisely to
protect the integrity of logged/audited statements, and Cf characters spoof
rendered text while comparing byte-different from the real catalog name
(Trojan Source, CVE-2021-42574). Same rule, same rationale, wider category.
- The XML docs on both files asserted that identifiers 'are sourced only from
catalog-validated (INFORMATION_SCHEMA) names' and that rejection is 'a
backstop, not the primary defence'. Neither is true yet: design §8.1 does
specify that gate, but nothing implements it and no task in the plan
schedules one, so an authored TagConfig table/column reaches QuoteIdentifier
unfiltered. The docs now say so, because under-scrutinising this method on
the strength of a filter that does not exist is exactly the failure mode.
- The control-character test only pinned NUL, so a regression to a
Contains('\0') check would still have passed. Pinned the whole Cc category
plus the new Cf rule; verified load-bearing by deleting the guard and
observing exactly the 5 new cases go red.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
The dialect owns the two things ADO.NET's System.Data.Common base types
cannot abstract: identifier quoting and the metadata-catalog SQL. It is
the driver's SQL-injection boundary — values always bind as DbParameters,
identifiers cannot, so the few that reach a command text are catalog-
sourced and pass through QuoteIdentifier.
QuoteIdentifier brackets one identifier part and doubles embedded ] (the
only metacharacter that can terminate a bracketed identifier early);
rejects null/empty/whitespace, >128 chars (T-SQL sysname ceiling), and any
control character (incl. NUL) as ArgumentException, without echoing the
untrusted value into the message. MapColumnType folds the design §3.7
families case-insensitively and never throws — an unrecognised family
falls back to String so a browse over an exotic column still renders;
timestamp/rowversion and time are deliberately NOT mapped to DateTime.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW