@@ -15,8 +15,14 @@ namespace Microsoft.KernelMemory.MemoryDb.SQLServer;
1515
1616/// <summary>
1717/// Represents a memory store implementation that uses a SQL Server database as its backing store.
18+ ///
19+ /// Security Notes:
20+ /// - All user-controlled input (index names) is normalized and validated using NormalizeIndexName()
21+ /// - Table names are properly escaped using GetFullTableName() with SQL bracket escaping
22+ /// - Index names are properly escaped using GetSafeIndexName() with SQL bracket escaping
23+ /// - Multiple layers of defense against SQL injection are implemented
1824/// </summary>
19- #pragma warning disable CA2100 // SQL reviewed for user input validation
25+ #pragma warning disable CA2100 // SQL reviewed for user input validation - comprehensive security measures implemented
2026public sealed class SqlServerMemory : IMemoryDb , IMemoryDbUpsertBatch , IDisposable
2127{
2228 /// <summary>
@@ -93,8 +99,8 @@ [vector_value] [float] NOT NULL
9399 FOREIGN KEY ([memory_id]) REFERENCES { this . GetFullTableName ( this . _config . MemoryTableName ) } ([id])
94100 );
95101
96- IF OBJECT_ID(N'[ { this . _config . Schema } .IXC_{ $ " { this . _config . EmbeddingsTableName } _{ index } "} ] ', N'U') IS NULL
97- CREATE CLUSTERED COLUMNSTORE INDEX [IXC_ { $ " { this . _config . EmbeddingsTableName } _{ index } "} ]
102+ IF OBJECT_ID(N'{ GetSafeIndexName ( $ " { this . _config . Schema } .IXC_{ this . _config . EmbeddingsTableName } _{ index } ") } ', N'U') IS NULL
103+ CREATE CLUSTERED COLUMNSTORE INDEX { GetSafeIndexName ( $ "IXC_ { this . _config . EmbeddingsTableName } _{ index } ") }
98104 ON { this . GetFullTableName ( $ "{ this . _config . EmbeddingsTableName } _{ index } ") }
99105 { ( this . _cachedServerVersion >= 16 ? "ORDER ([memory_id])" : "" ) } ;
100106
@@ -285,6 +291,10 @@ public async IAsyncEnumerable<MemoryRecord> GetListAsync(
285291 {
286292 var tagFilters = new TagCollection ( ) ;
287293
294+ // Security Note: The 'index' parameter used in this SQL construction is secured through:
295+ // 1. NormalizeIndexName() validation (only alphanumeric + hyphens allowed)
296+ // 2. GetFullTableName() SQL bracket escaping (] -> ]])
297+ // 3. GenerateFilters() uses proper parameterization
288298 command . CommandText = $@ "
289299 WITH [filters] AS
290300 (
@@ -359,6 +369,10 @@ SELECT TOP (@limit)
359369 try
360370 {
361371 var generatedFilters = this . GenerateFilters ( index , command . Parameters , filters ) ;
372+ // Security Note: The 'index' parameter used in this SQL construction is secured through:
373+ // 1. NormalizeIndexName() validation (only alphanumeric + hyphens allowed)
374+ // 2. GetFullTableName() SQL bracket escaping (] -> ]])
375+ // 3. Multiple layers of defense against SQL injection
362376 command . CommandText = $@ "
363377 WITH
364378 [embedding] as
@@ -553,6 +567,7 @@ public void Dispose()
553567
554568 // Note: "_" is allowed in SQL Server, but we normalize it to "-" for consistency with other DBs
555569 private static readonly Regex s_replaceIndexNameCharsRegex = new ( @"[\s|\\|/|.|_|:]" ) ;
570+ private static readonly Regex s_validIdentifierRegex = new ( @"^[a-zA-Z0-9\-]+$" ) ;
556571 private const string ValidSeparator = "-" ;
557572
558573 /// <summary>
@@ -662,10 +677,30 @@ private async Task<bool> DoesIndexExistsAsync(string indexName,
662677 /// Gets the full table name with schema.
663678 /// </summary>
664679 /// <param name="tableName">The table name.</param>
680+ /// <summary>
681+ /// Gets the full table name with schema and proper SQL escaping.
682+ /// Provides defense-in-depth by escaping potential injection characters.
683+ /// </summary>
684+ /// <param name="tableName">The table name.</param>
665685 /// <returns></returns>
666686 private string GetFullTableName ( string tableName )
667687 {
668- return $ "[{ this . _config . Schema } ].[{ tableName } ]";
688+ // Additional security: escape any square brackets in table name to prevent SQL injection
689+ // Even though we validate input, this provides defense in depth
690+ var escapedTableName = tableName . Replace ( "]" , "]]" ) ;
691+ return $ "[{ this . _config . Schema } ].[{ escapedTableName } ]";
692+ }
693+
694+ /// <summary>
695+ /// Gets a safely escaped index name for use in SQL DDL statements.
696+ /// </summary>
697+ /// <param name="indexName">The index name to escape</param>
698+ /// <returns>A safely escaped index name</returns>
699+ private static string GetSafeIndexName ( string indexName )
700+ {
701+ // Escape square brackets to prevent SQL injection in index names
702+ var escapedIndexName = indexName . Replace ( "]" , "]]" ) ;
703+ return $ "[{ escapedIndexName } ]";
669704 }
670705
671706 /// <summary>
@@ -757,12 +792,27 @@ private async Task<MemoryRecord> ReadEntryAsync(SqlDataReader dataReader, bool w
757792 return entry ;
758793 }
759794
795+ /// <summary>
796+ /// Normalizes and validates an index name to ensure it's safe for use in SQL queries.
797+ /// This method provides multiple layers of security against SQL injection:
798+ /// 1. Normalizes characters to safe alternatives
799+ /// 2. Validates that the result contains only safe characters (alphanumeric and hyphens)
800+ /// </summary>
801+ /// <param name="index">The index name to normalize</param>
802+ /// <returns>A normalized and validated index name safe for SQL usage</returns>
803+ /// <exception cref="ArgumentException">Thrown if the normalized index contains unsafe characters</exception>
760804 private static string NormalizeIndexName ( string index )
761805 {
762806 ArgumentNullExceptionEx . ThrowIfNullOrWhiteSpace ( index , nameof ( index ) , "The index name is empty" ) ;
763807
764808 index = s_replaceIndexNameCharsRegex . Replace ( index . Trim ( ) . ToLowerInvariant ( ) , ValidSeparator ) ;
765809
810+ // Additional security validation: ensure the normalized index contains only safe characters
811+ if ( ! s_validIdentifierRegex . IsMatch ( index ) )
812+ {
813+ throw new ArgumentException ( $ "Invalid index name after normalization: { index } . Only alphanumeric characters and hyphens are allowed.", nameof ( index ) ) ;
814+ }
815+
766816 return index ;
767817 }
768818
0 commit comments