diff --git a/csharp/ql/lib/semmle/code/csharp/security/dataflow/SqlInjectionQuery.qll b/csharp/ql/lib/semmle/code/csharp/security/dataflow/SqlInjectionQuery.qll index addc19321776..3f036881646f 100644 --- a/csharp/ql/lib/semmle/code/csharp/security/dataflow/SqlInjectionQuery.qll +++ b/csharp/ql/lib/semmle/code/csharp/security/dataflow/SqlInjectionQuery.qll @@ -87,3 +87,24 @@ private class ExternalSqlInjectionSanitizer extends Sanitizer { private class SimpleTypeSanitizer extends Sanitizer, SimpleTypeSanitizedExpr { } private class GuidSanitizer extends Sanitizer, GuidSanitizedExpr { } + +/** + * A sanitizer for Entity Framework Core's interpolated SQL methods. + * Methods like `FromSqlInterpolated` and `ExecuteSqlInterpolated` accept + * `FormattableString` parameters and properly parameterize interpolated values, + * making them safe from SQL injection. + */ +private class EfCoreInterpolatedSanitizer extends Sanitizer { + EfCoreInterpolatedSanitizer() { + exists(MethodCall mc | + mc.getTarget().getName() in [ + "FromSqlInterpolated", "ExecuteSqlInterpolated", "ExecuteSqlInterpolatedAsync" + ] and + mc.getTarget() + .getDeclaringType() + .hasQualifiedName("Microsoft.EntityFrameworkCore", + ["RelationalDatabaseFacadeExtensions", "RelationalQueryableExtensions"]) and + this.getExpr() = mc.getAnArgument() + ) + } +} diff --git a/csharp/ql/test/query-tests/Security Features/CWE-089/SqlInjectionEfCoreInterpolated.cs b/csharp/ql/test/query-tests/Security Features/CWE-089/SqlInjectionEfCoreInterpolated.cs new file mode 100644 index 000000000000..0dbb8f5830c9 --- /dev/null +++ b/csharp/ql/test/query-tests/Security Features/CWE-089/SqlInjectionEfCoreInterpolated.cs @@ -0,0 +1,50 @@ +namespace Test +{ + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Mvc; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Infrastructure; + + public class EfCoreInterpolatedTest : Controller + { + private DbContext _context; + + // GOOD: ExecuteSqlInterpolated properly parameterizes interpolated strings + public void SafeExecuteInterpolated(string userInput) + { + _context.Database.ExecuteSqlInterpolated($"DELETE FROM Users WHERE Name = {userInput}"); + } + + // BAD: ExecuteSqlRaw with string concatenation is vulnerable + public void UnsafeExecuteRaw(string userInput) + { + _context.Database.ExecuteSqlRaw("DELETE FROM Users WHERE Name = '" + userInput + "'"); + } + } +} + +namespace Microsoft.EntityFrameworkCore +{ + public class DbContext + { + public DatabaseFacade Database { get; } + } + + public class DbSet { } +} + +namespace Microsoft.EntityFrameworkCore.Infrastructure +{ + public class DatabaseFacade { } + + public static class RelationalDatabaseFacadeExtensions + { + public static int ExecuteSqlInterpolated(this DatabaseFacade database, System.FormattableString sql) => 0; + public static int ExecuteSqlRaw(this DatabaseFacade database, string sql, params object[] parameters) => 0; + } + + public static class RelationalQueryableExtensions + { + public static System.Linq.IQueryable FromSqlInterpolated(this DbSet source, System.FormattableString sql) where TEntity : class => null; + } +}