fix(sql): reject Unicode format chars in identifiers; stop overclaiming catalog validation
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
This commit is contained in:
@@ -11,10 +11,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
|
||||
/// Those live here, so no dialect assumption leaks into the reader, the poll planner, or the schema browser.
|
||||
/// <para><b>This interface is the driver's SQL-injection boundary.</b> Every runtime <em>value</em> is
|
||||
/// bound as a <see cref="DbParameter"/> and never reaches a command text. Identifiers cannot be
|
||||
/// parameterized in SQL, so the few that must appear as text are sourced only from catalog-validated
|
||||
/// (<c>INFORMATION_SCHEMA</c>) names and emitted through <see cref="QuoteIdentifier"/>. Any code path that
|
||||
/// builds SQL by concatenating an authored tag field <em>without</em> going through
|
||||
/// <see cref="QuoteIdentifier"/> is a defect (design §8.1).</para>
|
||||
/// parameterized in SQL, so the few that must appear as text are emitted through
|
||||
/// <see cref="QuoteIdentifier"/>. Any code path that builds SQL by concatenating an authored tag field
|
||||
/// <em>without</em> going through <see cref="QuoteIdentifier"/> is a defect (design §8.1).</para>
|
||||
/// <para><b>Not yet implemented:</b> design §8.1 also specifies that an authored table/column is
|
||||
/// validated against the live catalog before it is ever quoted into text, so that an unknown identifier
|
||||
/// rejects the tag. No such gate exists yet — an identifier reaches <see cref="QuoteIdentifier"/>
|
||||
/// straight from the authored <c>TagConfig</c> blob, so <b>quoting is currently the only defence</b>,
|
||||
/// not a backstop behind an upstream filter. Scrutinise it accordingly.</para>
|
||||
/// <para>Public because <c>Driver.Sql.Browser</c> consumes it — the catalog SQL <em>is</em> the browse
|
||||
/// engine, so it is shared rather than duplicated. Implementations own their provider package;
|
||||
/// <b>no provider-specific type appears in this signature</b> (<see cref="Factory"/> is the abstract
|
||||
|
||||
Reference in New Issue
Block a user