CVE-2024-39677 (C#, published 2024-07-08T15:15Z)
Description: value: NHibernate is an object-relational mapper for the .NET framework. A SQL injection vulnerability exists in some types implementing ILiteralType.ObjectToSQLString. Callers of these methods are exposed to the vulnerability, which includes mappings using inheritance with discriminator values; HQL queries referencing a static field of the application; users of the SqlInsertBuilder and SqlUpdateBuilder utilities, calling their AddColumn overload taking a literal value; and any direct use of the ObjectToSQLString methods for building SQL queries on the user side. This vulnerability is fixed in 5.4.9 and 5.5.2.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/nhibernate/nhibernate-core
Files changed:
doc/reference/modules/configuration.xml
src/NHibernate.Config.Templates/SapSQLAnywhere.cfg.xml
src/NHibernate.Test/Async/NHSpecificTest/GH3516/FixtureByCode.cs
src/NHibernate.Test/NHSpecificTest/GH3516/Entity.cs
src/NHibernate.Test/NHSpecificTest/GH3516/FixtureByCode.cs
src/NHibernate.Test/NHSpecificTest/GH3516/Hierarchy.cs
src/NHibernate.Test/SqlCommandTest/SqlInsertBuilderFixture.cs
src/NHibernate.Test/TestDialect.cs
src/NHibernate.Test/TestDialects/Oracle10gTestDialect.cs
src/NHibernate.Test/TestDialects/SapSQLAnywhere17TestDialect.cs
src/NHibernate/Async/Type/ByteType.cs
src/NHibernate/Async/Type/Int16Type.cs
src/NHibernate/Async/Type/Int32Type.cs
src/NHibernate/Async/Type/Int64Type.cs
src/NHibernate/Async/Type/TicksType.cs
src/NHibernate/Async/Type/TimeAsTimeSpanType.cs
src/NHibernate/Async/Type/TimeSpanType.cs
src/NHibernate/Async/Type/UInt16Type.cs
src/NHibernate/Async/Type/UInt32Type.cs
src/NHibernate/Async/Type/UInt64Type.cs
src/NHibernate/Cfg/Environment.cs
src/NHibernate/Dialect/DB2Dialect.cs
src/NHibernate/Dialect/Dialect.cs
src/NHibernate/Dialect/IngresDialect.cs
src/NHibernate/Dialect/MsSql2000Dialect.cs
src/NHibernate/Dialect/MySQLDialect.cs
src/NHibernate/Dialect/Oracle8iDialect.cs
src/NHibernate/Dialect/SybaseASA9Dialect.cs
src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs
src/NHibernate/Type/AbstractCharType.cs
src/NHibernate/Type/AbstractDateTimeType.cs
src/NHibernate/Type/AbstractStringType.cs
src/NHibernate/Type/ByteType.cs
src/NHibernate/Type/CharBooleanType.cs
src/NHibernate/Type/DateTimeOffSetType.cs
src/NHibernate/Type/DateType.cs
src/NHibernate/Type/DecimalType.cs
src/NHibernate/Type/DoubleType.cs
src/NHibernate/Type/EnumCharType.cs
src/NHibernate/Type/GuidType.cs
src/NHibernate/Type/Int16Type.cs
src/NHibernate/Type/Int32Type.cs
src/NHibernate/Type/Int64Type.cs
src/NHibernate/Type/SByteType.cs
src/NHibernate/Type/SingleType.cs
src/NHibernate/Type/TicksType.cs
src/NHibernate/Type/TimeAsTimeSpanType.cs
src/NHibernate/Type/TimeSpanType.cs
src/NHibernate/Type/TimeType.cs
src/NHibernate/Type/UInt16Type.cs
src/NHibernate/Type/UInt32Type.cs
src/NHibernate/Type/UInt64Type.cs
src/NHibernate/Type/UriType.cs
src/NHibernate/nhibernate-configuration.xsd
Commit message: Handle SQL injection vulnerabilities within ObjectToSQLString (#3547)

Co-authored-by: Alex Zaytsev <hazzik@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

Vulnerable code:
                        <row>
                            <entry>PostgreSQL</entry>
                            <entry><literal>NHibernate.Dialect.PostgreSQLDialect</literal></entry>
                            <entry>
                            </entry>
                        </row>
		<property name="dialect">NHibernate.Dialect.SybaseSQLAnywhere12Dialect</property>
			insert.AddColumn("stringColumn", "aSQLValue", (ILiteralType)NHibernateUtil.String);
		public bool SupportsSqlType(SqlType sqlType)
﻿using System.Runtime.InteropServices;
﻿namespace NHibernate.Test.TestDialects
		/// Does not support SELECT FOR UPDATE 
using System.Collections.Generic;
using System.Data;
using System.Collections.Generic;
using System.Data;
using System.Collections.Generic;
using System.Data;
using System.Collections.Generic;
using System.Data;
		/// <summary> The SQL literal value to which this database maps boolean values. </summary>
		/// <param name="value">The boolean value </param>
		/// <returns> The appropriate SQL literal. </returns>
		public virtual string ToBooleanValueString(bool value)
		{
			return value ? "1" : "0";
		}

	/// An SQL dialect for Sybase Adaptive Server Anywhere 9.0
		{
			return '\'' + value.ToString() + '\'';
		}
		public override string ObjectToSQLString(object value, Dialect.Dialect dialect) =>
			"'" + (DateTime) value + "'";
		{
			return "'" + (string)value + "'";
		}
			return value.ToString();
using NHibernate.Util;
		{
			return "'" + ToCharacter(value) + "'";
		}
		{
			return "'" + ((DateTimeOffset) value) + "'";
		}
		public override string ObjectToSQLString(object value, Dialect.Dialect dialect) =>
			"\'" + ((DateTime)value).ToShortDateString() + "\'";
			return value.ToString();
			return value.ToString();
		{
			return '\'' + GetValue(value).ToString() + '\'';
		}
			return "'" + value + "'";
using System.Collections.Generic;
using System.Data;
			return value.ToString();
using System.Collections.Generic;
using System.Data;
			return value.ToString();
			return value.ToString();
			return value.ToString();
			return value.ToString();
			return '\'' + ((DateTime)value).Ticks.ToString() + '\'';
using System.Collections.Generic;
using System.Data;
			return '\'' + ((TimeSpan)value).Ticks.ToString() + '\'';
using System.Collections.Generic;
using System.Data;
			return '\'' + ((TimeSpan)value).Ticks.ToString() + '\'';
		{
			return "'" + ((DateTime)value).ToShortTimeString() + "'";
		}
			return value.ToString();
			return value.ToString();
			return value.ToString();
		{
			return "'" + ((Uri)value).OriginalString + "'";
		}

Fixed code:
                    <row>
                        <entry>
                            <literal>escape_backslash_in_strings</literal>
                        </entry>
                        <entry>
                            Indicates if the database needs to have backslash escaped in string literals.
                            The default value is dialect dependant. That is <literal>false</literal> for
                            most dialects.
                            <para>
                                <emphasis role="strong">eg.</emphasis> 
                                <literal>true</literal> | <literal>false</literal>
                            </para>
                        </entry>
                    </row>
		<property name="dialect">NHibernate.Dialect.SapSQLAnywhere17Dialect</property>
﻿//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by AsyncGenerator.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;
using NHibernate.SqlTypes;
using NHibernate.Type;
using NUnit.Framework;
using NUnit.Framework.Internal;

namespace NHibernate.Test.NHSpecificTest.GH3516
{
	using System.Threading.Tasks;
	[TestFixture]
	public class FixtureByCodeAsync : TestCaseMappingByCode
	{

		private readonly HashSet<string> _unsupportedProperties = new();

		protected override HbmMapping GetMappings()
		{
			var mapper = new ModelMapper();
			mapper.Class<Entity>(rc =>
			{
				rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
				rc.Property(x => x.Name);
				rc.Property(x => x.FirstChar);
				rc.Property(x => x.CharacterEnum, m => m.Type<EnumCharType<CharEnum>>());
				rc.Property(x => x.UriProperty);

				rc.Property(x => x.ByteProperty);
				rc.Property(x => x.DecimalProperty);
				rc.Property(x => x.DoubleProperty);
				rc.Property(x => x.FloatProperty);
				rc.Property(x => x.ShortProperty);
				rc.Property(x => x.IntProperty);
				rc.Property(x => x.LongProperty);

				if (TestDialect.SupportsSqlType(SqlTypeFactory.SByte))
					rc.Property(x => x.SByteProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.SByteProperty));

				if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt16))
					rc.Property(x => x.UShortProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.UShortProperty));

				if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt32))
					rc.Property(x => x.UIntProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.UIntProperty));

				if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt64))
					rc.Property(x => x.ULongProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.ULongProperty));

				rc.Property(x => x.DateTimeProperty);
				rc.Property(x => x.DateProperty, m => m.Type(NHibernateUtil.Date));
				if (TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
					rc.Property(x => x.DateTimeOffsetProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.DateTimeOffsetProperty));
				rc.Property(x => x.TimeProperty, m => m.Type(NHibernateUtil.Time));

				rc.Property(x => x.GuidProperty);
			});

			mapper.Class<BaseClass>(rc =>
			{
				rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
				rc.Discriminator(x => x.Column("StringDiscriminator"));
				rc.Property(x => x.Name);
				rc.Abstract(true);
			});
			mapper.Subclass<Subclass1>(rc => rc.DiscriminatorValue(Entity.NameWithSingleQuote));
			mapper.Subclass<Subclass2>(rc => rc.DiscriminatorValue(Entity.NameWithEscapedSingleQuote));

			mapper.Import<CharEnum>();

			return mapper.CompileMappingForAllExplicitlyAddedEntities();
		}

		private CultureInfo _backupCulture;
		private CultureInfo _backupUICulture;

		protected override void OnSetUp()
		{
			using var session = OpenSession();
			using var transaction = session.BeginTransaction();
			session.Save(
				new Entity
				{
					Name = Entity.NameWithSingleQuote,
					FirstChar = Entity.QuoteInitial,
					CharacterEnum = CharEnum.SingleQuote,
					UriProperty = Entity.UriWithSingleQuote
				});
			session.Save(
				new Entity
				{
					Name = Entity.NameWithEscapedSingleQuote,
					FirstChar = Entity.BackslashInitial,
					CharacterEnum = CharEnum.Backslash,
					UriProperty = Entity.UriWithEscapedSingleQuote
				});

			transaction.Commit();

			_backupCulture = CultureInfo.CurrentCulture;
			_backupUICulture = CultureInfo.CurrentUICulture;
		}

		protected override void OnTearDown()
		{
			if (_backupCulture != null)
			{
				CultureInfo.CurrentCulture = _backupCulture;
				CultureInfo.CurrentUICulture = _backupUICulture;
			}

			using var session = OpenSession();
			using var transaction = session.BeginTransaction();
			session.CreateQuery("delete from System.Object").ExecuteUpdate();

			transaction.Commit();
		}

		private static readonly string[] StringInjectionsProperties =
		{
			nameof(Entity.NameWithSingleQuote), nameof(Entity.NameWithEscapedSingleQuote)
		};

		[TestCaseSource(nameof(StringInjectionsProperties))]
		public void SqlInjectionInStringsAsync(string propertyName)
		{
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.Name = Entity.{propertyName}");
			IList<Entity> list = null;
			Assert.That(async () => list = await (query.ListAsync<Entity>()), Throws.Nothing);
			Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with name {propertyName}");
		}

		private static readonly string[] SpecialNames =
		{
			"\0; drop table Entity; --",
			"\b; drop table Entity; --",
			"\n; drop table Entity; --",
			"\r; drop table Entity; --",
			"\t; drop table Entity; --",
			"\x1A; drop table Entity; --",
			"\"; drop table Entity; --",
			"\\; drop table Entity; --"
		};

		[TestCaseSource(nameof(SpecialNames))]
		public async Task StringsWithSpecialCharactersAsync(string name)
		{
			// We may not even be able to insert the entity.
			var wasInserted = false;
			try
			{
				using var s = OpenSession();
				using var t = s.BeginTransaction();
				var e = new Entity { Name = name };
				await (s.SaveAsync(e));
				await (t.CommitAsync());

				wasInserted = true;
			}
			catch (Exception e)
			{
				Assert.Warn($"The entity insertion failed with message {e}");
			}

			try
			{
				using var session = OpenSession();
				Entity.ArbitraryStringValue = name;
				var list = await (session.CreateQuery($"from Entity e where e.Name = Entity.{nameof(Entity.ArbitraryStringValue)}").ListAsync<Entity>());
				if (wasInserted && list.Count != 1)
					Assert.Warn($"Unable to find entity with name {nameof(Entity.ArbitraryStringValue)}");
			}
			catch (Exception e)
			{
				Assert.Warn($"The query has failed with message {e}");
			}

			// Check the db is not wrecked.
			if (wasInserted)
			{
				using var session = OpenSession();
				var list = await (session
					.CreateQuery("from Entity e where e.Name = :name")
					.SetString("name", name)
					.ListAsync<Entity>());
				Assert.That(list, Has.Count.EqualTo(1));
			}
			else
			{
				using var session = OpenSession();
				var all = await (session.CreateQuery("from Entity e").ListAsync<Entity>());
				Assert.That(all, Has.Count.GreaterThan(0));
			}
		}

		[Test]
		public async Task SqlInjectionInStringDiscriminatorAsync()
		{
			using var session = OpenSession();

			await (session.SaveAsync(new Subclass1 { Name = "Subclass1" }));
			await (session.SaveAsync(new Subclass2 { Name = "Subclass2" }));

			// ObjectToSQLString is used for generating the inserts.
			Assert.That(session.Flush, Throws.Nothing, "Unable to flush the subclasses");

			foreach (var entityName in new[] { nameof(Subclass1), nameof(Subclass2) })
			{
				var query = session.CreateQuery($"from {entityName}");
				IList list = null;
				Assert.That(async () => list = await (query.ListAsync()), Throws.Nothing, $"Unable to list entities of {entityName}");
				Assert.That(list, Has.Count.EqualTo(1), $"Unable to find the {entityName} entity");
			}
		}

		private static readonly string[] CharInjectionsProperties =
		{
			nameof(Entity.QuoteInitial), nameof(Entity.BackslashInitial)
		};

		[TestCaseSource(nameof(CharInjectionsProperties))]
		public void SqlInjectionInCharAsync(string propertyName)
		{
			using var session = OpenSession();
			var query = session.CreateQuery($"from Entity e where e.FirstChar = Entity.{propertyName}");
			IList<Entity> list = null;
			Assert.That(async () => list = await (query.ListAsync<Entity>()), Throws.Nothing);
			Assert.That(list, Is.Not.Null.And.Count.EqualTo(1), $"Unable to find entity with initial {propertyName}");
		}

		private static readonly string[] CharEnumInjections =
		{
			nameof(CharEnum.SingleQuote), nameof(CharEnum.Backslash)
		};

		[TestCaseSource(nameof(CharEnumInjections))]
		public void SqlInjectionWithCharEnumAsync(string enumName)
		{
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.CharacterEnum = CharEnum.{enumName}");
			IList<Entity> list = null;
			Assert.That(async () => list = await (query.ListAsync<Entity>()), Throws.Nothing);
			Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with CharacterEnum {enumName}");
		}

		private static readonly string[] UriInjections =
		{
			nameof(Entity.UriWithSingleQuote), nameof(Entity.UriWithEscapedSingleQuote)
		};

		[TestCaseSource(nameof(UriInjections))]
		public void SqlInjectionWithUriAsync(string propertyName)
		{
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.UriProperty = Entity.{propertyName}");
			IList<Entity> list = null;
			Assert.That(async () => list = await (query.ListAsync<Entity>()), Throws.Nothing);
			Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with UriProperty {propertyName}");
		}

		private static readonly string[] NumericalTypesInjections =
		{
			nameof(Entity.ByteProperty),
			nameof(Entity.DecimalProperty),
			nameof(Entity.DoubleProperty),
			nameof(Entity.FloatProperty),
			nameof(Entity.ShortProperty),
			nameof(Entity.IntProperty),
			nameof(Entity.LongProperty),
			nameof(Entity.SByteProperty),
			nameof(Entity.UShortProperty),
			nameof(Entity.UIntProperty),
			nameof(Entity.ULongProperty)
		};

		[TestCaseSource(nameof(NumericalTypesInjections))]
		public async Task SqlInjectionInNumericalTypeAsync(string propertyName)
		{
			Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect");

			Entity.ArbitraryStringValue = "0; drop table Entity; --";
			using (var session = OpenSession())
			{
				IQuery query;
				// Defining that query is invalid and should throw.
				try
				{
					query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{nameof(Entity.ArbitraryStringValue)}");
				}
				catch (Exception ex)
				{
					// All good.
					Assert.Pass($"The wicked query creation has been rejected, as it should: {ex}");
					// Needed for the compiler who does not know "Pass" always throw.
					return;
				}

				// The query definition has been accepted, run it.
				try
				{
					await (query.ListAsync<Entity>());
				}
				catch (Exception ex)
				{
					// Expecting no exception at that point, but the test is to check if the injection succeeded.
					Assert.Warn($"The wicked query execution has failed: {ex}");
				}
			}

			// Check if we can still query Entity. If it succeeds, at least it means the injection failed.
			using (var session = OpenSession())
			{
				IList<Entity> list = null;
				Assert.That(async () => list = await (session.CreateQuery("from Entity e").ListAsync<Entity>()), Throws.Nothing);
				Assert.That(list, Has.Count.GreaterThan(0));
			}
		}

		private static readonly string[] DateTypesInjections =
		{
			nameof(Entity.DateTimeProperty),
			nameof(Entity.DateProperty),
			nameof(Entity.DateTimeOffsetProperty),
			nameof(Entity.TimeProperty)
		};

		[TestCaseSource(nameof(DateTypesInjections))]
		public void SqlInjectionWithDatetimeAsync(string propertyName)
		{
			Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect");

			var wickedCulture = new CultureInfo("en-US");
			if (propertyName == nameof(Entity.TimeProperty))
				wickedCulture.DateTimeFormat.ShortTimePattern = "HH:mm:ss\\'\"; drop table Entity; --\"";
			else
				wickedCulture.DateTimeFormat.ShortDatePattern = "yyyy-MM-ddTHH:mm:ss\\'\"; drop table Entity; --\"";
			CultureInfo.CurrentCulture = wickedCulture;
			CultureInfo.CurrentUICulture = wickedCulture;

			using var session = OpenSession();

			var staticPropertyName = propertyName == nameof(Entity.DateTimeOffsetProperty) ?
				nameof(Entity.StaticDateTimeOffsetProperty) : nameof(Entity.StaticDateProperty);
			var query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{staticPropertyName}");
			IList<Entity> list = null;
			Assume.That(() => list = query.List<Entity>(), Throws.Nothing,
				"The first execution of the query failed, the injection has likely failed");
			// Execute again to check the table is still here.
			Assert.That(async () => list = await (query.ListAsync<Entity>()), Throws.Nothing,
				"The second execution of the query failed although the first one did not: the injection has succeeded");
		}

		private static readonly string[] GuidInjections =
		{
			Entity.NameWithSingleQuote, Entity.NameWithEscapedSingleQuote
		};

		[TestCaseSource(nameof(GuidInjections))]
		public void SqlInjectionWithGuidAsync(string injection)
		{
			Entity.ArbitraryStringValue = $"{Guid.NewGuid()}{injection}";
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.GuidProperty = Entity.{nameof(Entity.ArbitraryStringValue)}");
			IList<Entity> list = null;
			Assume.That(() => list = query.List<Entity>(), Throws.Nothing,
				"The first execution of the query failed, the injection has likely failed");
			// Execute again to check the table is still here.
			Assert.That(async () => list = await (query.ListAsync<Entity>()), Throws.Nothing,
				"The second execution of the query failed although the first one did not: the injection has succeeded");
		}
	}
}
﻿using System;

namespace NHibernate.Test.NHSpecificTest.GH3516
{
	public class Entity
	{
		public virtual Guid Id { get; set; }
		public virtual string Name { get; set; }
		public virtual char FirstChar { get; set; }
		public virtual CharEnum CharacterEnum { get; set; } = CharEnum.SimpleChar;
		public virtual Uri UriProperty { get; set; }

		public virtual byte ByteProperty { get; set; }
		public virtual decimal DecimalProperty { get; set; }
		public virtual double DoubleProperty { get; set; }
		public virtual float FloatProperty { get; set; }
		public virtual short ShortProperty { get; set; }
		public virtual int IntProperty { get; set; }
		public virtual long LongProperty { get; set; }
		public virtual sbyte SByteProperty { get; set; }
		public virtual ushort UShortProperty { get; set; }
		public virtual uint UIntProperty { get; set; }
		public virtual ulong ULongProperty { get; set; }

		public virtual DateTime DateTimeProperty { get; set; } = StaticDateProperty;
		public virtual DateTime DateProperty { get; set; } = StaticDateProperty;
		public virtual DateTimeOffset DateTimeOffsetProperty { get; set; } = StaticDateProperty;
		public virtual DateTime TimeProperty { get; set; } = StaticDateProperty;

		public virtual Guid GuidProperty { get; set; } = Guid.Empty;

		public const string NameWithSingleQuote = "'; drop table Entity; --";
		public const string NameWithEscapedSingleQuote = @"\'; drop table Entity; --";

		// Do not switch to property, the feature of referencing static fields in HQL does not work with properties.
		public static string ArbitraryStringValue;

		public const char QuoteInitial = '\'';
		public const char BackslashInitial = '\\';

		public static readonly Uri UriWithSingleQuote = new("https://somewhere/?sql='; drop table Entity; --");
		public static readonly Uri UriWithEscapedSingleQuote = new(@"https://somewhere/?sql=\'; drop table Entity; --");

		public static readonly DateTime StaticDateProperty = DateTime.Today;
		public static readonly DateTimeOffset StaticDateTimeOffsetProperty = DateTimeOffset.Now;
	}

	public enum CharEnum
	{
		SimpleChar = 'A',
		SingleQuote = '\'',
		Backslash = '\\'
	}
}
﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Mapping.ByCode;
using NHibernate.SqlTypes;
using NHibernate.Type;
using NUnit.Framework;
using NUnit.Framework.Internal;

namespace NHibernate.Test.NHSpecificTest.GH3516
{
	[TestFixture]
	public class FixtureByCode : TestCaseMappingByCode
	{

		private readonly HashSet<string> _unsupportedProperties = new();

		protected override HbmMapping GetMappings()
		{
			var mapper = new ModelMapper();
			mapper.Class<Entity>(rc =>
			{
				rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
				rc.Property(x => x.Name);
				rc.Property(x => x.FirstChar);
				rc.Property(x => x.CharacterEnum, m => m.Type<EnumCharType<CharEnum>>());
				rc.Property(x => x.UriProperty);

				rc.Property(x => x.ByteProperty);
				rc.Property(x => x.DecimalProperty);
				rc.Property(x => x.DoubleProperty);
				rc.Property(x => x.FloatProperty);
				rc.Property(x => x.ShortProperty);
				rc.Property(x => x.IntProperty);
				rc.Property(x => x.LongProperty);

				if (TestDialect.SupportsSqlType(SqlTypeFactory.SByte))
					rc.Property(x => x.SByteProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.SByteProperty));

				if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt16))
					rc.Property(x => x.UShortProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.UShortProperty));

				if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt32))
					rc.Property(x => x.UIntProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.UIntProperty));

				if (TestDialect.SupportsSqlType(SqlTypeFactory.UInt64))
					rc.Property(x => x.ULongProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.ULongProperty));

				rc.Property(x => x.DateTimeProperty);
				rc.Property(x => x.DateProperty, m => m.Type(NHibernateUtil.Date));
				if (TestDialect.SupportsSqlType(SqlTypeFactory.DateTimeOffSet))
					rc.Property(x => x.DateTimeOffsetProperty);
				else
					_unsupportedProperties.Add(nameof(Entity.DateTimeOffsetProperty));
				rc.Property(x => x.TimeProperty, m => m.Type(NHibernateUtil.Time));

				rc.Property(x => x.GuidProperty);
			});

			mapper.Class<BaseClass>(rc =>
			{
				rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
				rc.Discriminator(x => x.Column("StringDiscriminator"));
				rc.Property(x => x.Name);
				rc.Abstract(true);
			});
			mapper.Subclass<Subclass1>(rc => rc.DiscriminatorValue(Entity.NameWithSingleQuote));
			mapper.Subclass<Subclass2>(rc => rc.DiscriminatorValue(Entity.NameWithEscapedSingleQuote));

			mapper.Import<CharEnum>();

			return mapper.CompileMappingForAllExplicitlyAddedEntities();
		}

		private CultureInfo _backupCulture;
		private CultureInfo _backupUICulture;

		protected override void OnSetUp()
		{
			using var session = OpenSession();
			using var transaction = session.BeginTransaction();
			session.Save(
				new Entity
				{
					Name = Entity.NameWithSingleQuote,
					FirstChar = Entity.QuoteInitial,
					CharacterEnum = CharEnum.SingleQuote,
					UriProperty = Entity.UriWithSingleQuote
				});
			session.Save(
				new Entity
				{
					Name = Entity.NameWithEscapedSingleQuote,
					FirstChar = Entity.BackslashInitial,
					CharacterEnum = CharEnum.Backslash,
					UriProperty = Entity.UriWithEscapedSingleQuote
				});

			transaction.Commit();

			_backupCulture = CultureInfo.CurrentCulture;
			_backupUICulture = CultureInfo.CurrentUICulture;
		}

		protected override void OnTearDown()
		{
			if (_backupCulture != null)
			{
				CultureInfo.CurrentCulture = _backupCulture;
				CultureInfo.CurrentUICulture = _backupUICulture;
			}

			using var session = OpenSession();
			using var transaction = session.BeginTransaction();
			session.CreateQuery("delete from System.Object").ExecuteUpdate();

			transaction.Commit();
		}

		private static readonly string[] StringInjectionsProperties =
		{
			nameof(Entity.NameWithSingleQuote), nameof(Entity.NameWithEscapedSingleQuote)
		};

		[TestCaseSource(nameof(StringInjectionsProperties))]
		public void SqlInjectionInStrings(string propertyName)
		{
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.Name = Entity.{propertyName}");
			IList<Entity> list = null;
			Assert.That(() => list = query.List<Entity>(), Throws.Nothing);
			Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with name {propertyName}");
		}

		private static readonly string[] SpecialNames =
		{
			"\0; drop table Entity; --",
			"\b; drop table Entity; --",
			"\n; drop table Entity; --",
			"\r; drop table Entity; --",
			"\t; drop table Entity; --",
			"\x1A; drop table Entity; --",
			"\"; drop table Entity; --",
			"\\; drop table Entity; --"
		};

		[TestCaseSource(nameof(SpecialNames))]
		public void StringsWithSpecialCharacters(string name)
		{
			// We may not even be able to insert the entity.
			var wasInserted = false;
			try
			{
				using var s = OpenSession();
				using var t = s.BeginTransaction();
				var e = new Entity { Name = name };
				s.Save(e);
				t.Commit();

				wasInserted = true;
			}
			catch (Exception e)
			{
				Assert.Warn($"The entity insertion failed with message {e}");
			}

			try
			{
				using var session = OpenSession();
				Entity.ArbitraryStringValue = name;
				var list = session.CreateQuery($"from Entity e where e.Name = Entity.{nameof(Entity.ArbitraryStringValue)}").List<Entity>();
				if (wasInserted && list.Count != 1)
					Assert.Warn($"Unable to find entity with name {nameof(Entity.ArbitraryStringValue)}");
			}
			catch (Exception e)
			{
				Assert.Warn($"The query has failed with message {e}");
			}

			// Check the db is not wrecked.
			if (wasInserted)
			{
				using var session = OpenSession();
				var list = session
					.CreateQuery("from Entity e where e.Name = :name")
					.SetString("name", name)
					.List<Entity>();
				Assert.That(list, Has.Count.EqualTo(1));
			}
			else
			{
				using var session = OpenSession();
				var all = session.CreateQuery("from Entity e").List<Entity>();
				Assert.That(all, Has.Count.GreaterThan(0));
			}
		}

		[Test]
		public void SqlInjectionInStringDiscriminator()
		{
			using var session = OpenSession();

			session.Save(new Subclass1 { Name = "Subclass1" });
			session.Save(new Subclass2 { Name = "Subclass2" });

			// ObjectToSQLString is used for generating the inserts.
			Assert.That(session.Flush, Throws.Nothing, "Unable to flush the subclasses");

			foreach (var entityName in new[] { nameof(Subclass1), nameof(Subclass2) })
			{
				var query = session.CreateQuery($"from {entityName}");
				IList list = null;
				Assert.That(() => list = query.List(), Throws.Nothing, $"Unable to list entities of {entityName}");
				Assert.That(list, Has.Count.EqualTo(1), $"Unable to find the {entityName} entity");
			}
		}

		private static readonly string[] CharInjectionsProperties =
		{
			nameof(Entity.QuoteInitial), nameof(Entity.BackslashInitial)
		};

		[TestCaseSource(nameof(CharInjectionsProperties))]
		public void SqlInjectionInChar(string propertyName)
		{
			using var session = OpenSession();
			var query = session.CreateQuery($"from Entity e where e.FirstChar = Entity.{propertyName}");
			IList<Entity> list = null;
			Assert.That(() => list = query.List<Entity>(), Throws.Nothing);
			Assert.That(list, Is.Not.Null.And.Count.EqualTo(1), $"Unable to find entity with initial {propertyName}");
		}

		private static readonly string[] CharEnumInjections =
		{
			nameof(CharEnum.SingleQuote), nameof(CharEnum.Backslash)
		};

		[TestCaseSource(nameof(CharEnumInjections))]
		public void SqlInjectionWithCharEnum(string enumName)
		{
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.CharacterEnum = CharEnum.{enumName}");
			IList<Entity> list = null;
			Assert.That(() => list = query.List<Entity>(), Throws.Nothing);
			Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with CharacterEnum {enumName}");
		}

		private static readonly string[] UriInjections =
		{
			nameof(Entity.UriWithSingleQuote), nameof(Entity.UriWithEscapedSingleQuote)
		};

		[TestCaseSource(nameof(UriInjections))]
		public void SqlInjectionWithUri(string propertyName)
		{
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.UriProperty = Entity.{propertyName}");
			IList<Entity> list = null;
			Assert.That(() => list = query.List<Entity>(), Throws.Nothing);
			Assert.That(list, Has.Count.EqualTo(1), $"Unable to find entity with UriProperty {propertyName}");
		}

		private static readonly string[] NumericalTypesInjections =
		{
			nameof(Entity.ByteProperty),
			nameof(Entity.DecimalProperty),
			nameof(Entity.DoubleProperty),
			nameof(Entity.FloatProperty),
			nameof(Entity.ShortProperty),
			nameof(Entity.IntProperty),
			nameof(Entity.LongProperty),
			nameof(Entity.SByteProperty),
			nameof(Entity.UShortProperty),
			nameof(Entity.UIntProperty),
			nameof(Entity.ULongProperty)
		};

		[TestCaseSource(nameof(NumericalTypesInjections))]
		public void SqlInjectionInNumericalType(string propertyName)
		{
			Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect");

			Entity.ArbitraryStringValue = "0; drop table Entity; --";
			using (var session = OpenSession())
			{
				IQuery query;
				// Defining that query is invalid and should throw.
				try
				{
					query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{nameof(Entity.ArbitraryStringValue)}");
				}
				catch (Exception ex)
				{
					// All good.
					Assert.Pass($"The wicked query creation has been rejected, as it should: {ex}");
					// Needed for the compiler who does not know "Pass" always throw.
					return;
				}

				// The query definition has been accepted, run it.
				try
				{
					query.List<Entity>();
				}
				catch (Exception ex)
				{
					// Expecting no exception at that point, but the test is to check if the injection succeeded.
					Assert.Warn($"The wicked query execution has failed: {ex}");
				}
			}

			// Check if we can still query Entity. If it succeeds, at least it means the injection failed.
			using (var session = OpenSession())
			{
				IList<Entity> list = null;
				Assert.That(() => list = session.CreateQuery("from Entity e").List<Entity>(), Throws.Nothing);
				Assert.That(list, Has.Count.GreaterThan(0));
			}
		}

		private static readonly string[] DateTypesInjections =
		{
			nameof(Entity.DateTimeProperty),
			nameof(Entity.DateProperty),
			nameof(Entity.DateTimeOffsetProperty),
			nameof(Entity.TimeProperty)
		};

		[TestCaseSource(nameof(DateTypesInjections))]
		public void SqlInjectionWithDatetime(string propertyName)
		{
			Assume.That(_unsupportedProperties, Does.Not.Contains((object) propertyName), $"The {propertyName} property is unsupported by the dialect");

			var wickedCulture = new CultureInfo("en-US");
			if (propertyName == nameof(Entity.TimeProperty))
				wickedCulture.DateTimeFormat.ShortTimePattern = "HH:mm:ss\\'\"; drop table Entity; --\"";
			else
				wickedCulture.DateTimeFormat.ShortDatePattern = "yyyy-MM-ddTHH:mm:ss\\'\"; drop table Entity; --\"";
			CultureInfo.CurrentCulture = wickedCulture;
			CultureInfo.CurrentUICulture = wickedCulture;

			using var session = OpenSession();

			var staticPropertyName = propertyName == nameof(Entity.DateTimeOffsetProperty) ?
				nameof(Entity.StaticDateTimeOffsetProperty) : nameof(Entity.StaticDateProperty);
			var query = session.CreateQuery($"from Entity e where e.{propertyName} = Entity.{staticPropertyName}");
			IList<Entity> list = null;
			Assume.That(() => list = query.List<Entity>(), Throws.Nothing,
				"The first execution of the query failed, the injection has likely failed");
			// Execute again to check the table is still here.
			Assert.That(() => list = query.List<Entity>(), Throws.Nothing,
				"The second execution of the query failed although the first one did not: the injection has succeeded");
		}

		private static readonly string[] GuidInjections =
		{
			Entity.NameWithSingleQuote, Entity.NameWithEscapedSingleQuote
		};

		[TestCaseSource(nameof(GuidInjections))]
		public void SqlInjectionWithGuid(string injection)
		{
			Entity.ArbitraryStringValue = $"{Guid.NewGuid()}{injection}";
			using var session = OpenSession();

			var query = session.CreateQuery($"from Entity e where e.GuidProperty = Entity.{nameof(Entity.ArbitraryStringValue)}");
			IList<Entity> list = null;
			Assume.That(() => list = query.List<Entity>(), Throws.Nothing,
				"The first execution of the query failed, the injection has likely failed");
			// Execute again to check the table is still here.
			Assert.That(() => list = query.List<Entity>(), Throws.Nothing,
				"The second execution of the query failed although the first one did not: the injection has succeeded");
		}
	}
}
﻿using System;

namespace NHibernate.Test.NHSpecificTest.GH3516
{
	public class BaseClass
	{
		public virtual Guid Id { get; set; }
		public virtual string Name { get; set; }
	}

	public class Subclass1 : BaseClass
	{
	}

	public class Subclass2 : BaseClass
	{
	}
}
			insert.AddColumn("stringColumn", "aSQLValue", (ILiteralType)NHibernateUtil.AnsiString);
		public virtual bool SupportsSqlType(SqlType sqlType)
﻿using System.Data;
using System.Runtime.InteropServices;
using NHibernate.SqlTypes;
		public override bool SupportsSqlType(SqlType sqlType)
		{
			// The Oracle dialects define types for DbType the Oracle driver does not support.
			return sqlType.DbType switch
			{
				DbType.UInt16 or DbType.UInt32 or DbType.UInt64 => false,
				_ => base.SupportsSqlType(sqlType)
			};
		}

﻿using System.Data;
using NHibernate.SqlTypes;

namespace NHibernate.Test.TestDialects
		/// Does not support SELECT FOR UPDATE.

		public override bool SupportsSqlType(SqlType sqlType)
		{
			// The Anywhere dialects define types for DbType the Anywhere driver does not support.
			return sqlType.DbType switch
			{
				DbType.SByte => false,
				_ => base.SupportsSqlType(sqlType)
			};
		}
using System.Globalization;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Globalization;
using System.Globalization;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Globalization;
using System.Globalization;
using System.Globalization;
		/// <summary>
		/// Indicates if the database needs to have backslash escaped in string literals.
		/// </summary>
		/// <remarks>The default value is dialect dependent.</remarks>
		public const string EscapeBackslashInStrings = "escape_backslash_in_strings";

using NHibernate.SqlTypes;
		/// <inheritdoc />
		public override string ToStringLiteral(string value, SqlType type)
		{
			if (value == null)
				throw new System.ArgumentNullException(nameof(value));
			if (type == null)
				throw new System.ArgumentNullException(nameof(value));

			// See https://www.ibm.com/docs/en/db2/11.5?topic=elements-constants#r0000731__title__7
			var literal = new StringBuilder(value);
			var isUnicode = type.DbType == DbType.String || type.DbType == DbType.StringFixedLength;
			if (isUnicode)
				literal.Replace(@"\", @"\\");

			literal
				.Replace("'", "''")
				.Insert(0, '\'')
				.Append('\'');

			if (isUnicode)
				literal.Insert(0, "U&");
			return literal.ToString();
		}

			EscapeBackslashInStrings = PropertiesHelper.GetBoolean(Environment.EscapeBackslashInStrings, settings, EscapeBackslashInStrings);
		#region Literals support

		/// <summary>The SQL literal value to which this database maps boolean values.</summary>
		/// <param name="value">The boolean value.</param>
		/// <returns>The appropriate SQL literal.</returns>
		public virtual string ToBooleanValueString(bool value)
			=> value ? "1" : "0";

		/// <summary>
		/// <see langword="true" /> if the database needs to have backslash escaped in string literals.
		/// </summary>
		/// <remarks><see langword="false" /> by default in the base dialect, to conform to SQL standard.</remarks>
		protected virtual bool EscapeBackslashInStrings { get; set; }

		/// <summary>
		/// <see langword="true" /> if the database needs to have Unicode literals prefixed by <c>N</c>.
		/// </summary>
		/// <remarks><see langword="false" /> by default in the base dialect.</remarks>
		protected virtual bool UseNPrefixForUnicodeStrings => false;

		/// <summary>The SQL string literal value to which this database maps string values.</summary>
		/// <param name="value">The string value.</param>
		/// <param name="type">The SQL type of the string value.</param>
		/// <returns>The appropriate SQL string literal.</returns>
		/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> or
		/// <paramref name="type"/> is <see langword="null" />.</exception>
		public virtual string ToStringLiteral(string value, SqlType type)
		{
			if (value == null)
				throw new ArgumentNullException(nameof(value));
			if (type == null)
				throw new ArgumentNullException(nameof(type));

			var literal = new StringBuilder(value);
			if (EscapeBackslashInStrings)
				literal.Replace(@"\", @"\\");

			literal
				.Replace("'", "''")
				.Insert(0, '\'')
				.Append('\'');

			if (UseNPrefixForUnicodeStrings && (type.DbType == DbType.String || type.DbType == DbType.StringFixedLength))
				literal.Insert(0, 'N');
			return literal.ToString();
		}

		#endregion

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for Ingres,
		/// <see href="https://docs.actian.com/ingres/11.0/index.html#page/SQLRef/String_Literals.htm#ww110572" />.</remarks>
		protected override bool UseNPrefixForUnicodeStrings => true;

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for SQL Server.</remarks>
		protected override bool UseNPrefixForUnicodeStrings => true;

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for MySQL,
		/// <see href="https://dev.mysql.com/doc/refman/8.0/en/string-literals.html" />.</remarks>
		protected override bool EscapeBackslashInStrings { get; set; } = true;

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for MySQL,
		/// <see href="https://dev.mysql.com/doc/refman/8.0/en/string-literals.html" />.</remarks>
		protected override bool UseNPrefixForUnicodeStrings => true;


		/// <inheritdoc />
		/// <remarks>Returns the same value as <see cref="UseNPrefixedTypesForUnicode" />.</remarks>
		protected override bool UseNPrefixForUnicodeStrings => UseNPrefixedTypesForUnicode;

	/// An SQL dialect for Sybase Adaptive Server Anywhere 9.0. (Renamed SQL Anywhere from its version 10.)

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for SQL Anywhere,
		/// <see href="https://help.sap.com/docs/SAP_SQL_Anywhere/93079d4ba8e44920ae63ffb4def91f5b/817a3ded6ce21014bd99f3e554573180.html?version=17.0" />.</remarks>
		protected override bool EscapeBackslashInStrings { get; set; } = true;

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for SQL Anywhere,
		/// <see href="https://help.sap.com/docs/SAP_SQL_Anywhere/93079d4ba8e44920ae63ffb4def91f5b/817a2c5f6ce21014aceea962de72126c.html?version=17.0" />.</remarks>
		protected override bool UseNPrefixForUnicodeStrings => true;

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for SQL Anywhere,
		/// <see href="https://help.sap.com/docs/SAP_SQL_Anywhere/93079d4ba8e44920ae63ffb4def91f5b/817a3ded6ce21014bd99f3e554573180.html?version=17.0" />.</remarks>
		protected override bool EscapeBackslashInStrings { get; set; } = true;

		/// <inheritdoc />
		/// <remarks><see langword="true" /> by default for SQL Anywhere,
		/// <see href="https://help.sap.com/docs/SAP_SQL_Anywhere/93079d4ba8e44920ae63ffb4def91f5b/817a2c5f6ce21014aceea962de72126c.html?version=17.0" />.</remarks>
		protected override bool UseNPrefixForUnicodeStrings => true;
			=> dialect.ToStringLiteral(value.ToString(), SqlType);
		public override string ObjectToSQLString(object value, Dialect.Dialect dialect)
			=> dialect.ToStringLiteral(((DateTime) value).ToString(), SqlTypeFactory.GetAnsiString(50));
		/// <inheritdoc />
			=> dialect.ToStringLiteral((string)value, SqlType);
using System.Globalization;
			return ((byte)value).ToString(CultureInfo.InvariantCulture);
			=> dialect.ToStringLiteral(ToCharacter(value), SqlType);
			=> dialect.ToStringLiteral(((DateTimeOffset) value).ToString(), SqlTypeFactory.GetAnsiString(50));
		public override string ObjectToSQLString(object value, Dialect.Dialect dialect)
			=> dialect.ToStringLiteral(((DateTime) value).ToShortDateString(), SqlTypeFactory.GetAnsiString(50));
using System.Globalization;
			return ((decimal)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return ((double)value).ToString(CultureInfo.InvariantCulture);
			=> dialect.ToStringLiteral(GetValue(value).ToString(), SqlType);
			return dialect.ToStringLiteral(value.ToString(), SqlTypeFactory.GetAnsiString(50));
using System.Collections.Generic;
using System.Data;
using System.Globalization;
			return ((short)value).ToString(CultureInfo.InvariantCulture);
using System.Collections.Generic;
using System.Data;
using System.Globalization;
			return ((int)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return ((long)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return ((sbyte)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return ((float)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return '\'' + ((DateTime)value).Ticks.ToString(CultureInfo.InvariantCulture) + '\'';
using System.Collections.Generic;
using System.Data;
using System.Globalization;
			return '\'' + ((TimeSpan)value).Ticks.ToString(CultureInfo.InvariantCulture) + '\'';
using System.Collections.Generic;
using System.Data;
using System.Globalization;
			return '\'' + ((TimeSpan)value).Ticks.ToString(CultureInfo.InvariantCulture) + '\'';
			=> dialect.ToStringLiteral(((DateTime) value).ToShortTimeString(), SqlTypeFactory.GetAnsiString(50));
using System.Globalization;
			return ((ushort)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return ((uint)value).ToString(CultureInfo.InvariantCulture);
using System.Globalization;
			return ((ulong)value).ToString(CultureInfo.InvariantCulture);
			=> dialect.ToStringLiteral(((Uri) value).OriginalString, SqlType);
										<xs:enumeration value="escape_backslash_in_strings">
											<xs:annotation>
												<xs:documentation>
													Indicates if the database needs to have backslash escaped in string literals. The default is
													dialect dependent.
												</xs:documentation>
											</xs:annotation>
										</xs:enumeration>