Skip to content

Handle SQL injection vulnerabilities within ObjectToSQLString #3547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d4b9daf
Add injection test cases
fredericDelaporte May 14, 2024
a3619ac
Add a discriminator injection test
fredericDelaporte Jun 9, 2024
b7a153a
Add a test for special characters
fredericDelaporte May 19, 2024
d2ff013
Minor code cleanup
hazzik Jun 3, 2024
d480863
Add test for char
fredericDelaporte May 26, 2024
61a9549
Minor code cleanup
hazzik Jun 3, 2024
897a869
Initial is reserved keyword in oracle
hazzik Jun 3, 2024
f6c3988
Add a charenum injection test
fredericDelaporte Jun 9, 2024
d7677c6
Add an Uri injection test case
fredericDelaporte Jun 9, 2024
69c4c12
Add numerical types injection test cases
fredericDelaporte Jun 9, 2024
d85c5a6
Add a datetime test case
fredericDelaporte Jun 10, 2024
4cf9fd3
Escapes string in AbstractStringType
fredericDelaporte May 12, 2024
2d21ff3
Fix argument name
hazzik Jun 3, 2024
0dcbfca
Fix a test failing due to new Unicode support
fredericDelaporte May 13, 2024
2da9e9e
Fix the char type
fredericDelaporte May 26, 2024
02bcc42
Fix types handled as SQL strings
fredericDelaporte Jun 9, 2024
edc4177
Add a minimal fix for numeric types
fredericDelaporte Jun 9, 2024
77fe3e5
Minimal fix for the datetime case
fredericDelaporte Jun 10, 2024
aa91eb7
Disallow culture injection for numeric types
fredericDelaporte Jun 11, 2024
03936a2
Disallow culture injecton in ticks dependent types
fredericDelaporte Jun 11, 2024
b7c0576
Generate async files
github-actions[bot] Jun 11, 2024
ea888be
Switch to cast instead of convert
fredericDelaporte Jun 12, 2024
27bc4bf
Add injection test for other datetime types
fredericDelaporte Jun 12, 2024
0c35063
Fix other datetime types
fredericDelaporte Jun 12, 2024
e80b766
Add a Guid injection test
fredericDelaporte Jun 12, 2024
a1cebb2
Fix the Guid type
fredericDelaporte Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Escapes string in AbstractStringType
  • Loading branch information
fredericDelaporte committed Jun 10, 2024
commit 4cf9fd38aa3b17b20f46d70be9596ae7cbaa5a72
20 changes: 14 additions & 6 deletions doc/reference/modules/configuration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,20 @@ var session = sessions.OpenSession(conn);
</para>
</entry>
</row>
<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>
<row>
<entry>
<literal>show_sql</literal>
Expand Down Expand Up @@ -1515,12 +1529,6 @@ in the parameter binding.</programlisting>
<entry><literal>NHibernate.Dialect.PostgreSQLDialect</literal></entry>
<entry></entry>
</row>
<row>
<entry>PostgreSQL</entry>
<entry><literal>NHibernate.Dialect.PostgreSQLDialect</literal></entry>
<entry>
</entry>
</row>
<row>
<entry>PostgreSQL 8.1</entry>
<entry><literal>NHibernate.Dialect.PostgreSQL81Dialect</literal></entry>
Expand Down
6 changes: 6 additions & 0 deletions src/NHibernate/Cfg/Environment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ public static string Version
/// <summary> Enable formatting of SQL logged to the console</summary>
public const string FormatSql = "format_sql";

/// <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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be a user configurable option in my opinion. The dialect should perform the all appropriate escaping where necessary. For example, your implementation for DB2 ignores this setting while always escaping back slashes for Unicode strings.


// Since v5.0.1
[Obsolete("This setting has no usages and will be removed in a future version")]
public const string UseGetGeneratedKeys = "jdbc.use_get_generated_keys";
Expand Down
25 changes: 25 additions & 0 deletions src/NHibernate/Dialect/DB2Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NHibernate.Dialect.Function;
using NHibernate.Dialect.Schema;
using NHibernate.SqlCommand;
using NHibernate.SqlTypes;

namespace NHibernate.Dialect
{
Expand Down Expand Up @@ -296,6 +297,30 @@ public override string ForUpdateString

public override long TimestampResolutionInTicks => 10L; // Microseconds.

/// <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&");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other databases supports that escaping, but only DB2 documentation seemed to imply Unicode string literals were supported only by using it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if this code avoided multiple iterations over the initial value. The longer the value and more escaping added, the more expensive this operation could become. I would recommend similar changes elsewhere.

return literal.ToString();
}

#region Overridden informational metadata

public override bool SupportsNullInUnique => false;
Expand Down
58 changes: 50 additions & 8 deletions src/NHibernate/Dialect/Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ public virtual void Configure(IDictionary<string, string> settings)
DefaultCastLength = PropertiesHelper.GetInt32(Environment.QueryDefaultCastLength, settings, 4000);
DefaultCastPrecision = PropertiesHelper.GetByte(Environment.QueryDefaultCastPrecision, settings, null) ?? 29;
DefaultCastScale = PropertiesHelper.GetByte(Environment.QueryDefaultCastScale, settings, null) ?? 10;
EscapeBackslashInStrings = PropertiesHelper.GetBoolean(Environment.EscapeBackslashInStrings, settings, EscapeBackslashInStrings);
}

#endregion
Expand Down Expand Up @@ -1354,14 +1355,6 @@ public virtual CaseFragment CreateCaseFragment()
return new ANSICaseFragment(this);
}

/// <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";
}

internal static void ExtractColumnOrAliasNames(SqlString select, out List<SqlString> columnsOrAliases, out Dictionary<SqlString, SqlString> aliasToColumn, out Dictionary<SqlString, SqlString> columnToAlias)
{
columnsOrAliases = new List<SqlString>();
Expand Down Expand Up @@ -2076,6 +2069,55 @@ public virtual string ConvertQuotesForCatalogName(string catalogName)

#endregion

#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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sold on necessity of passing the SqlType here. The primary purpose of this value is to determine whether or not the value to be escaped should result in a Unicode value. I would suggest one of the following:

  1. Create a separate method called ToUnicodeStringLiteral.
  2. Pass a boolean value indicating whether or not to return a Unicode string literal.

Admittedly, I prefer the former over the latter as it allows dialects to have different implementations if needed without a conditional on the boolean value provided.

{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (type == null)
throw new ArgumentNullException(nameof(value));

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no guarantee that the implementation of this method won't break an obscure dialect that does not support doubling the number of single quotes inside a single quoted string to escape a single quote. While this is the ANSI standard, not all dialects adhere to that standard.

The only non-breaking thing to do here is to simply return the value passed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing a bug of lack of escapism is anyway behavior breaking for those which were compensating it by escaping values themselves (if anyone).

And from a security standpoint, better take the risk of breaking some dialects not natively supported by NHibernate rather than leaving the most common vulnerability (lack of doubling the quote) open for all not natively supported dialects.

Fixing bug is anyway always somewhat behavior breaking. That is normal and accepted for patch releases. That is binary breaking changes or source breaking changes which are banned by SemVer for a patch or minor release.

Mores specifically, from https://semver.org/ :

Patch version Z (x.y.Z | x > 0) MUST be incremented if only backward compatible bug fixes are introduced. A bug fix is defined as an internal change that fixes incorrect behavior.

(So, a new throwing base method causing previously "working in most cases" features to cease working is neither a binary breaking change nor a source one. But undoubtedly it does not qualify as "an internal change that fixes incorrect behavior".)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, you are saying that in the case of a bug fix, it's okay to throw an exception for changes that correct behavior. Admittedly, I do not have a problem with that so long as the exception is thrown in the appropriate location. The changes here would cause an exception to be thrown once the statement is executed, not when the method is called. Any dialects broken by this change would therefore not know the root cause of the exception. Therefore, if you'd like to throw an exception in the base case instead of leaving external dialects vulnerable, that would be acceptable.

Alternatively, as already discussed, NHibernate could use query parameters to avoid escaping any values. It may not be as performant as the solution here, but it would undoubtedly be more maintainable and secure. It would also not introduce any breaking changes.

Copy link
Member Author

@fredericDelaporte fredericDelaporte May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I am not saying that:

you are saying that in the case of a bug fix, it's okay to throw an exception for changes that correct behavior.


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

#endregion

#region Union subclass support

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions src/NHibernate/Dialect/IngresDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public IngresDialect()
/// <inheritdoc />
public override int MaxAliasLength => 32;

/// <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;

#region Overridden informational metadata

public override bool SupportsEmptyInList => false;
Expand Down
4 changes: 4 additions & 0 deletions src/NHibernate/Dialect/MsSql2000Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,10 @@ public override bool SupportsSqlBatches
/// </summary>
public override int? MaxNumberOfParameters => 2097;

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

#region Overridden informational metadata

public override bool SupportsEmptyInList => false;
Expand Down
10 changes: 10 additions & 0 deletions src/NHibernate/Dialect/MySQLDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,16 @@ public override long TimestampResolutionInTicks
/// </remarks>
public override bool SupportsConcurrentWritingConnectionsInSameTransaction => false;

/// <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;

#region Overridden informational metadata

public override bool SupportsEmptyInList => false;
Expand Down
5 changes: 5 additions & 0 deletions src/NHibernate/Dialect/Oracle8iDialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public override void Configure(IDictionary<string, string> settings)

// If changing the default value, keep it in sync with OracleDataClientDriverBase.Configure.
UseNPrefixedTypesForUnicode = PropertiesHelper.GetBoolean(Environment.OracleUseNPrefixedTypesForUnicode, settings, false);

RegisterCharacterTypeMappings();
RegisterFloatingPointTypeMappings();
}
Expand Down Expand Up @@ -561,6 +562,10 @@ public override long TimestampResolutionInTicks
/// <inheritdoc />
public override int MaxAliasLength => 30;

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

#region Overridden informational metadata

public override bool SupportsEmptyInList
Expand Down
12 changes: 11 additions & 1 deletion src/NHibernate/Dialect/SybaseASA9Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace NHibernate.Dialect
{
/// <summary>
/// An SQL dialect for Sybase Adaptive Server Anywhere 9.0
/// An SQL dialect for Sybase Adaptive Server Anywhere 9.0. (Renamed SQL Anywhere from its version 10.)
/// </summary>
/// <remarks>
/// <p>
Expand Down Expand Up @@ -188,5 +188,15 @@ private static int GetAfterSelectInsertPoint(SqlString sql)
}
return 0;
}

/// <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;
}
}
10 changes: 10 additions & 0 deletions src/NHibernate/Dialect/SybaseSQLAnywhere10Dialect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -969,5 +969,15 @@ public override IDataBaseSchema GetDataBaseSchema(DbConnection connection)
/// <inheritdoc />
/// <remarks>SQL Anywhere has a micro-second resolution.</remarks>
public override long TimestampResolutionInTicks => 10L;

/// <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;
}
}
5 changes: 2 additions & 3 deletions src/NHibernate/Type/AbstractStringType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,9 @@ public object StringToObject(string xml)

#region ILiteralType Members

/// <inheritdoc />
public string ObjectToSQLString(object value, Dialect.Dialect dialect)
{
return "'" + (string)value + "'";
}
=> dialect.ToStringLiteral((string)value, SqlType);

#endregion

Expand Down
8 changes: 8 additions & 0 deletions src/NHibernate/nhibernate-configuration.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@
<xs:enumeration value="default_flush_mode" />
<xs:enumeration value="use_sql_comments" />
<xs:enumeration value="format_sql" />
<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>
<xs:enumeration value="collectiontype.factory_class" />
<xs:enumeration value="order_inserts" />
<xs:enumeration value="order_updates" />
Expand Down