VYPR
Critical severity9.8NVD Advisory· Published May 14, 2026· Updated May 14, 2026

Marten has an injection vulnerability in its full-text search regConfig parameter

CVE-2026-45288

Description

Summary

Marten's full-text search APIs interpolated the user-supplied regConfig parameter directly into the generated SQL without parameterization or validation, making every code path that exposes regConfig to untrusted input a SQL injection sink.

Affected

APIs

  • IQuerySession.SearchAsync<T>(string searchTerm, string regConfig, ...)
  • IQuerySession.PlainTextSearchAsync<T>(...)
  • IQuerySession.PhraseSearchAsync<T>(...)
  • IQuerySession.WebStyleSearchAsync<T>(...)
  • IQuerySession.PrefixSearchAsync<T>(...)
  • IQueryable<T>.Where(x => x.Search(term, regConfig)) and the matching PlainTextSearch / PhraseSearch / WebStyleSearch / PrefixSearch extension methods

Details

In the affected versions, `FullTextWhereFragment` renders the WHERE-clause SQL by string interpolation:

private string Sql =>
    $"to_tsvector('{_regConfig}'::regconfig, {_dataConfig}) @@ {_searchFunction}('{_regConfig}'::regconfig, ?)";

_regConfig arrives unchanged from the public API surface above. Any value containing a single quote terminates the SQL literal and lets an attacker append arbitrary PostgreSQL.

Confirmed exploit shapes (with regConfig set to attacker-controlled input)

| Goal | Payload | | --- | --- | | Time-based blind | english'::text); SELECT pg_sleep(5); -- | | Information disclosure | english'; SELECT version(); -- | | DDL execution | english'; DROP TABLE mt_doc_article; -- |

All five overloads listed above produced SQL containing the verbatim payload.

Impact

  • Confidentiality: an attacker can append arbitrary SELECT statements and exfiltrate database contents through error channels, response timing, or — if the application surfaces query results — directly.
  • Integrity / Availability: DDL, UPDATE, DELETE, and pg_sleep-style denial-of-service payloads succeed under the same vector. Concrete impact depends on the database role used by the Marten connection string.
  • Precondition: the calling application must forward attacker-controlled input into the regConfig parameter (e.g. a ?lang= query string mapped to regConfig). Applications that hard-code regConfig to a compile-time constant are not exploitable.

Patches

Fixed in Marten 8.36.1 (and forward) by #4343.

FullTextWhereFragment now validates regConfig against ^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$ (a simple PostgreSQL identifier, optionally schema-qualified, capped at NAMEDATALEN-1 per side) and throws ArgumentException for anything else. The default value ("english"), schema-qualified configs ("pg_catalog.english"), and the standard PostgreSQL text-search configurations all continue to work.

Workarounds

If users cannot upgrade immediately, do one of the following at the application boundary:

  1. Hard-code regConfig to a compile-time constant ("english", "simple", …) and never accept it from request input.
  2. Validate any externally-sourced regConfig value before passing it to Marten — e.g. against the same regex as the patch (^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$) or against an allowlist of PostgreSQL configurations the application actually uses.
  3. Drop the regConfig argument from the call site so Marten falls back to the safe default.

Resources

Credit

Reported privately to the JasperFx team with a working proof of concept covering all five affected overloads.

Patches

1
626249656829

Reject non-identifier regConfig values in full-text search (SQL injection fix) (#4343)

https://github.com/JasperFx/martenJeremy D. MillerMay 8, 2026via ghsa
2 files changed · +150 0
  • src/LinqTests/Bugs/full_text_regconfig_sql_injection.cs+118 0 added
    @@ -0,0 +1,118 @@
    +using System;
    +using System.Linq;
    +using JasperFx;
    +using Marten;
    +using Marten.Linq;
    +using Marten.Testing.Harness;
    +using Shouldly;
    +using Xunit;
    +
    +namespace LinqTests.Bugs;
    +
    +// Reproduces and locks down the SQL injection vector reported against the
    +// regConfig parameter on Marten's full-text search APIs. Pre-fix, the regConfig
    +// string was interpolated directly into the generated SQL by
    +// FullTextWhereFragment, making every overload below a sink for arbitrary
    +// PostgreSQL syntax: time-based blind, information disclosure, DDL, etc.
    +//
    +// The tests below assert that a regConfig value containing an obvious payload
    +// (single quote, semicolon, comment marker, sleep call) does NOT survive into
    +// the generated SQL — either by being rejected at query-construction time
    +// (preferred) or by being stripped/escaped. Either outcome breaks the
    +// injection vector.
    +public class full_text_regconfig_sql_injection
    +{
    +    private const string TimeBasedBlindPayload = "english'::text); SELECT pg_sleep(5); --";
    +    private const string ExfiltrationPayload = "english'; SELECT version(); --";
    +    private const string DdlPayload = "english'; DROP TABLE mt_doc_article; --";
    +
    +    public class Article
    +    {
    +        public Guid Id { get; set; }
    +        public string Title { get; set; } = string.Empty;
    +        public string Body { get; set; } = string.Empty;
    +    }
    +
    +    private static IDocumentStore BuildStore() => DocumentStore.For(opts =>
    +    {
    +        opts.Connection(ConnectionSource.ConnectionString);
    +        opts.AutoCreateSchemaObjects = AutoCreate.None;
    +    });
    +
    +    // The full set of full-text search overloads that take a user-controllable
    +    // regConfig argument and route through FullTextWhereFragment.
    +    public static TheoryData<string, Func<IQueryable<Article>, string, IQueryable<Article>>> Overloads => new()
    +    {
    +        { nameof(LinqExtensions.Search),          (q, rc) => q.Where(x => x.Search("term", rc)) },
    +        { nameof(LinqExtensions.PlainTextSearch), (q, rc) => q.Where(x => x.PlainTextSearch("term", rc)) },
    +        { nameof(LinqExtensions.PhraseSearch),    (q, rc) => q.Where(x => x.PhraseSearch("term", rc)) },
    +        { nameof(LinqExtensions.WebStyleSearch),  (q, rc) => q.Where(x => x.WebStyleSearch("term", rc)) },
    +        { nameof(LinqExtensions.PrefixSearch),    (q, rc) => q.Where(x => x.PrefixSearch("term", rc)) },
    +    };
    +
    +    private static readonly string[] InjectionPayloads =
    +    [
    +        TimeBasedBlindPayload,
    +        ExfiltrationPayload,
    +        DdlPayload,
    +    ];
    +
    +    [Theory]
    +    [MemberData(nameof(Overloads))]
    +    public void rejects_injection_payloads_in_regConfig(
    +        string overloadName,
    +        Func<IQueryable<Article>, string, IQueryable<Article>> apply)
    +    {
    +        _ = overloadName;
    +        using var store = BuildStore();
    +        using var session = store.LightweightSession();
    +
    +        foreach (var payload in InjectionPayloads)
    +        {
    +            var query = apply(session.Query<Article>(), payload);
    +
    +            // Either the query refuses to materialise (validation throws), or
    +            // the rendered SQL must NOT contain the raw payload. Both outcomes
    +            // close the injection vector; we accept either.
    +            string? sql = null;
    +            try
    +            {
    +                sql = query.ToCommand(FetchType.FetchMany).CommandText;
    +            }
    +            catch (ArgumentException)
    +            {
    +                // Acceptable: validation rejected the input before SQL was generated.
    +                continue;
    +            }
    +
    +            // None of these tokens should ever appear in SQL generated from a
    +            // legitimate full-text search; their presence means the payload was
    +            // interpolated verbatim. The matched substring is what would
    +            // otherwise execute on the database.
    +            sql.ShouldNotContain("pg_sleep", Case.Insensitive);
    +            sql.ShouldNotContain("DROP TABLE", Case.Insensitive);
    +            sql.ShouldNotContain("SELECT version", Case.Insensitive);
    +            sql.ShouldNotContain("--");
    +        }
    +    }
    +
    +    [Theory]
    +    [InlineData("english")]
    +    [InlineData("french")]
    +    [InlineData("simple")]
    +    [InlineData("pg_catalog.english")]
    +    public void accepts_known_safe_regConfig_values(string regConfig)
    +    {
    +        using var store = BuildStore();
    +        using var session = store.LightweightSession();
    +
    +        // None of these should throw; all should produce valid SQL referencing
    +        // the regconfig name (so we know the parameter is still being honored).
    +        var sql = session.Query<Article>()
    +            .Where(x => x.PlainTextSearch("term", regConfig))
    +            .ToCommand(FetchType.FetchMany)
    +            .CommandText;
    +
    +        sql.ShouldContain(regConfig);
    +    }
    +}
    
  • src/Marten/Linq/SqlGeneration/Filters/FullTextWhereFragment.cs+32 0 modified
    @@ -1,5 +1,7 @@
     #nullable enable
    +using System;
     using System.Linq;
    +using System.Text.RegularExpressions;
     using Marten.Linq.Parsing.Methods.FullText;
     using Marten.Schema;
     using Weasel.Postgresql;
    @@ -10,6 +12,17 @@ namespace Marten.Linq.SqlGeneration.Filters;
     
     internal class FullTextWhereFragment: ISqlFragment
     {
    +    // PostgreSQL text-search configuration names are stored as identifiers in
    +    // pg_ts_config (see https://www.postgresql.org/docs/current/textsearch-configuration.html).
    +    // We allow simple unquoted identifiers — optionally schema-qualified — so values
    +    // like "english", "french", or "pg_catalog.english" pass through, while anything
    +    // containing whitespace, quotes, semicolons, or other punctuation is rejected.
    +    // This is a security-critical check: regConfig is interpolated into SQL by Sql below,
    +    // so any value that escapes this pattern would be a SQL injection sink.
    +    private static readonly Regex _regConfigPattern = new(
    +        @"^[a-zA-Z_][a-zA-Z0-9_]{0,62}(\.[a-zA-Z_][a-zA-Z0-9_]{0,62})?$",
    +        RegexOptions.Compiled | RegexOptions.CultureInvariant);
    +
         private readonly string _dataConfig;
         private readonly string _regConfig;
         private readonly FullTextSearchFunction _searchFunction;
    @@ -18,13 +31,32 @@ internal class FullTextWhereFragment: ISqlFragment
         public FullTextWhereFragment(DocumentMapping? mapping, FullTextSearchFunction searchFunction, string searchTerm,
             string regConfig = FullTextIndexDefinition.DefaultRegConfig)
         {
    +        ValidateRegConfig(regConfig);
    +
             _regConfig = regConfig;
     
             _dataConfig = GetDataConfig(mapping, regConfig).Replace("data", "d.data");
             _searchFunction = searchFunction;
             _searchTerm = searchTerm;
         }
     
    +    private static void ValidateRegConfig(string regConfig)
    +    {
    +        if (regConfig is null)
    +        {
    +            throw new ArgumentNullException(nameof(regConfig));
    +        }
    +
    +        if (!_regConfigPattern.IsMatch(regConfig))
    +        {
    +            throw new ArgumentException(
    +                $"Invalid PostgreSQL text-search configuration name '{regConfig}'. " +
    +                "regConfig must be a simple PostgreSQL identifier (optionally schema-qualified), " +
    +                "matching ^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)?$.",
    +                nameof(regConfig));
    +        }
    +    }
    +
         // don't parameterize full-text search config as it ruins the performance with the query plan in PG
         private string Sql =>
             $"to_tsvector('{_regConfig}'::regconfig, {_dataConfig}) @@ {_searchFunction}('{_regConfig}'::regconfig, ?)";
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

4

News mentions

0

No linked articles in our index yet.