Several code paths in Marten's LINQ provider and tenant-management internals interpolated a runtime, potentially attacker-influenced value into generated SQL as a single-quoted string literal without escaping or parameterization. A value containing a single quote (') could break out of the literal and inject arbitrary SQL.
The primary vector is a Dictionary<,> indexer key used in a Where filter — Where(x => x.Attributes[key] == v) — a common "filter by attribute name" / EAV pattern. It is confirmed with an executed proof-of-concept and yields filter / multi-tenant authorization bypass (returning other tenants' rows) and blind data exfiltration. Where the application permits ;-batched statements (Npgsql default), data modification is also possible.
Linq/Members/Dictionaries/DictionaryItemMember.cs — dictionary indexer key (primary, confirmed PoC)Linq/Members/Dictionaries/DictionaryContainsKeyFilter.cs — Dictionary.ContainsKey(key) (Newtonsoft serializer only; System.Text.Json escapes the quote)Linq/Parsing/SelectParser.cs — a constant string projected through Select(x => new { L = runtimeString })Internal/Operations/DeleteAllForTenant.cs — tenant id reaching per-tenant projection teardown via IEventStore.DeleteProjectionProgressAsync (an in-code comment wrongly claimed the value was validated)Schema/DatabaseScopedTenantPartitions.cs — tenant id inlined into FOR VALUES IN ('...') partition DDLEvents/Daemon/Internals/EventLoader.cs (defense-in-depth) — per-tenant partition-pruning literalBenign: x.Attributes["nonexistent-key"] == "v" returns 0 rows. Attack: x.Attributes["nonexistent' = '' or 1=1 --"] == "v" returns all rows. Generated SQL:
select d.data from public.mt_doc_doc as d where d.data -> 'Attributes' ->> 'x' or 1=1 --' = :p0;
Each sink now escapes embedded single quotes (.Replace("'", "''"), mirroring the existing Ordering.BuildNgramRankExpression) or binds the value as a parameter (DeleteAllForTenant). Where a literal is retained for partition pruning, escaping preserves plan-time pruning while closing the injection. Regression tests lock down each vector.
Until upgrading: do not pass untrusted input as a dictionary indexer key / ContainsKey argument / Select constant, nor as a tenant id into projection teardown or provisioning; and disable multi-statement command batching to limit blast radius.
Reported privately and responsibly by an external security researcher with an executed PoC. Additional sinks found during the follow-up LINQ-wide audit.
{
"cwe_ids": [
"CWE-89"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-17T14:51:41Z",
"nvd_published_at": "2026-09-16T21:17:13Z",
"severity": "CRITICAL"
}