C#: Model EntityFramework

This commit is contained in:
calum
2019-02-06 11:27:12 +00:00
committed by Calum Grant
parent 7010ca8cf3
commit 74b30d6071
17 changed files with 490 additions and 45 deletions

View File

@@ -9,6 +9,7 @@ module DataFlow {
private import semmle.code.csharp.dataflow.CallContext
private import semmle.code.csharp.dataflow.DelegateDataFlow
private import semmle.code.csharp.dataflow.LibraryTypeDataFlow
private import semmle.code.csharp.frameworks.EntityFramework
private import Internal::Cached
private import dotnet
private import cil
@@ -117,6 +118,15 @@ module DataFlow {
predicate localFlowStep = Internal::LocalFlow::step/2;
/**
* A dataflow node that jumps between callables. This can be extended in framework code
* to add additional dataflow steps.
*/
abstract class NonLocalJumpNode extends Node {
/** Gets a successor node that is potentially in another callable. */
abstract Node getAJumpSuccessor();
}
/**
* A data flow node augmented with a call context and a configuration. Only
* nodes that are reachable from a source, and which can reach a sink, are
@@ -1398,12 +1408,26 @@ module DataFlow {
*/
cached
predicate jumpStep(ExprNode pred, ExprNode succ) {
exists(FieldLike fl, FieldLikeRead flr | fl.isStatic() |
fl.getAnAssignedValue() = pred.getExpr() and
pred.(NonLocalJumpNode).getAJumpSuccessor() = succ
}
/** A dataflow node that has field-like dataflow. */
private class FieldLikeJumpNode extends NonLocalJumpNode, ExprNode {
FieldLike fl;
FieldLikeRead flr;
ExprNode succ;
FieldLikeJumpNode() {
fl.isStatic() and
fl.getAnAssignedValue() = this.getExpr() and
fl.getAnAccess() = flr and
flr = succ.getExpr() and
hasNonlocalValue(flr)
)
}
override ExprNode getAJumpSuccessor() { result = succ }
}
/**

View File

@@ -0,0 +1,192 @@
/**
* Classes modelling EntityFramework and EntityFrameworkCore.
*/
import csharp
private import semmle.code.csharp.frameworks.system.data.Entity
private import semmle.code.csharp.frameworks.system.collections.Generic
private import semmle.code.csharp.frameworks.Sql
private import semmle.code.csharp.dataflow.LibraryTypeDataFlow
module DataAnnotations {
class NotMappedAttribute extends Attribute {
NotMappedAttribute() {
this
.getType()
.hasQualifiedName("System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute")
}
}
}
module EntityFramework {
/** An EF6 or EFCore namespace. */
class EFNamespace extends Namespace {
EFNamespace() {
this.getQualifiedName() = "Microsoft.EntityFrameworkCore"
or
this.getQualifiedName() = "System.Data.Entity"
}
}
/** A taint source where the data has come from a mapped property stored in the database. */
class StoredFlowSource extends DataFlow::Node {
StoredFlowSource() {
this.asExpr() = any(PropertyRead read | read.getTarget() instanceof MappedProperty)
}
}
private class EFClass extends Class {
EFClass() { this.getDeclaringNamespace() instanceof EFNamespace }
}
/** The class `Microsoft.EntityFrameworkCore.DbContext` or `System.Data.Entity.DbContext`. */
class DbContext extends EFClass {
DbContext() { this.getName() = "DbContext" }
Method getAFindMethod() {
result = this.getAMethod("Find")
or
result = this.getAMethod("FindAsync")
}
Method getAnUpdateMethod() { result = this.getAMethod("Update") }
}
/** The class `Microsoft.EntityFrameworkCore.DbSet<>` or `System.Data.Entity.DbSet<>`. */
class DbSet extends EFClass, UnboundGenericClass { DbSet() { this.getName() = "DbSet<>" } }
/** The class `Microsoft.EntityFrameworkCore.DbQuery<>` or `System.Data.Entity.DbQuery<>`. */
class DbQuery extends EFClass, UnboundGenericClass { DbQuery() { this.hasName("DbQuery<>") } }
/** A generic type or method that takes a mapped type as its type argument. */
private predicate usesMappedType(UnboundGeneric g) {
g instanceof DbSet
or
g instanceof DbQuery
or
exists(DbContext db |
g = db.getAnUpdateMethod()
or
g = db.getAFindMethod()
)
}
/** A type that is mapped to database table, or used as a query. */
class MappedType extends ValueOrRefType {
MappedType() {
not this instanceof ObjectType and
not this instanceof StringType and
not this instanceof ValueType and
(
exists(UnboundGeneric g | usesMappedType(g) |
this = g.getAConstructedGeneric().getATypeArgument()
)
or
this.getASubType() instanceof MappedType
)
}
}
/** A property that is potentially stored and retrieved from a database. */
class MappedProperty extends Property {
MappedProperty() {
this = any(MappedType t).getAMember() and
this.isPublic() and
not this.getAnAttribute() instanceof DataAnnotations::NotMappedAttribute
}
}
/** The struct `Microsoft.EntityFrameworkCore.RawSqlString`. */
class RawSqlStringStruct extends Struct, LibraryTypeDataFlow {
RawSqlStringStruct() { this.getQualifiedName() = "Microsoft.EntityFrameworkCore.RawSqlString" }
override predicate callableFlow(
CallableFlowSource source, CallableFlowSink sink, SourceDeclarationCallable c,
boolean preservesValue
) {
c = this.getAConstructor() and
source.(CallableFlowSourceArg).getArgumentIndex() = 0 and
sink instanceof CallableFlowSinkReturn and
preservesValue = true
or
c = this.getAConversionTo() and
source.(CallableFlowSourceArg).getArgumentIndex() = 0 and
sink instanceof CallableFlowSinkReturn and
preservesValue = true
}
ConversionOperator getAConversionTo() {
result = this.getAMember() and
result.getTargetType() instanceof RawSqlStringStruct and
result.getSourceType() instanceof StringType
}
}
/**
* A parameter that accepts raw SQL. Parameters of type `System.FormattableString`
* are not included as they are not vulnerable to SQL injection.
*/
private class SqlParameter extends Parameter {
SqlParameter() {
this.getType() instanceof StringType and
(
exists(Callable c | this = c.getParameter(0) | c.getName().matches("%Sql"))
or
this.getName() = "sql"
) and
this.getCallable().getDeclaringType().getDeclaringNamespace().getParentNamespace*() instanceof
EFNamespace
or
this.getType() instanceof RawSqlStringStruct
or
this = any(RawSqlStringStruct s).getAConstructor().getAParameter()
or
this = any(RawSqlStringStruct s).getAConversionTo().getAParameter()
}
}
/** A call to a method in EntityFrameworkCore that executes SQL. */
class EntityFrameworkCoreSqlSink extends SqlExpr, Call {
SqlParameter sqlParam;
EntityFrameworkCoreSqlSink() { this.getTarget().getAParameter() = sqlParam }
override Expr getSql() { result = this.getArgumentForParameter(sqlParam) }
}
/** A call to `System.Data.Entity.DbSet.SqlQuery`. */
class SystemDataEntityDbSetSqlExpr extends SqlExpr, MethodCall {
SystemDataEntityDbSetSqlExpr() {
this.getTarget() = any(SystemDataEntity::DbSet dbSet).getSqlQueryMethod()
}
override Expr getSql() { result = this.getArgumentForName("sql") }
}
/** A call to a method in `System.Data.Entity.Database` that executes SQL. */
class SystemDataEntityDatabaseSqlExpr extends SqlExpr, MethodCall {
SystemDataEntityDatabaseSqlExpr() {
exists(SystemDataEntity::Database db |
this.getTarget() = db.getSqlQueryMethod() or
this.getTarget() = db.getExecuteSqlCommandMethod() or
this.getTarget() = db.getExecuteSqlCommandAsyncMethod()
)
}
override Expr getSql() { result = this.getArgumentForName("sql") }
}
/**
* A dataflow node whereby data flows from a property write to a property read
* via some database. The assumption is that all writes can flow to all reads.
*/
class MappedPropertyJumpNode extends DataFlow::NonLocalJumpNode {
MappedProperty property;
MappedPropertyJumpNode() { this.asExpr() = property.getAnAssignedValue() }
override DataFlow::Node getAJumpSuccessor() {
result.asExpr().(PropertyRead).getTarget() = property
}
}
}

View File

@@ -2,8 +2,8 @@
import csharp
private import semmle.code.csharp.frameworks.system.Data
private import semmle.code.csharp.frameworks.system.data.Entity
private import semmle.code.csharp.frameworks.system.data.SqlClient
private import semmle.code.csharp.frameworks.EntityFramework
/** An expression containing a SQL command. */
abstract class SqlExpr extends Expr {
@@ -22,7 +22,7 @@ class CommandTextAssignmentSqlExpr extends SqlExpr, AssignExpr {
)
}
override Expr getSql() { result = getRValue() }
override Expr getSql() { result = this.getRValue() }
}
/** A construction of an `IDbCommand` object. */
@@ -34,7 +34,7 @@ class IDbCommandConstructionSqlExpr extends SqlExpr, ObjectCreation {
)
}
override Expr getSql() { result = getArgument(0) }
override Expr getSql() { result = this.getArgument(0) }
}
/** A construction of an `SqlDataAdapter` object. */
@@ -47,7 +47,7 @@ class SqlDataAdapterConstructionSqlExpr extends SqlExpr, ObjectCreation {
)
}
override Expr getSql() { result = getArgument(0) }
override Expr getSql() { result = this.getArgument(0) }
}
/** A `MySql.Data.MySqlClient.MySqlHelper` method. */
@@ -83,25 +83,3 @@ class MicrosoftSqlHelperMethodCallSqlExpr extends SqlExpr, MethodCall {
)
}
}
/** A call to `System.Data.Entity.DbSet.SqlQuery`. */
class SystemDataEntityDbSetSqlExpr extends SqlExpr, MethodCall {
SystemDataEntityDbSetSqlExpr() {
this.getTarget() = any(SystemDataEntity::DbSet dbSet).getSqlQueryMethod()
}
override Expr getSql() { result = getArgumentForName("sql") }
}
/** A call to a method in `System.Data.Entity.Database` that executes SQL. */
class SystemDataEntityDatabaseSqlExpr extends SqlExpr, MethodCall {
SystemDataEntityDatabaseSqlExpr() {
exists(SystemDataEntity::Database db |
this.getTarget() = db.getSqlQueryMethod() or
this.getTarget() = db.getExecuteSqlCommandMethod() or
this.getTarget() = db.getExecuteSqlCommandAsyncMethod()
)
}
override Expr getSql() { result = getArgumentForName("sql") }
}

View File

@@ -5,18 +5,12 @@
import csharp
private import semmle.code.csharp.frameworks.system.data.Common
private import semmle.code.csharp.frameworks.system.data.Entity
private import semmle.code.csharp.frameworks.EntityFramework
private import semmle.code.csharp.frameworks.Sql
/** A data flow source of stored user input. */
abstract class StoredFlowSource extends DataFlow::Node { }
/** An access of an Entity Framework `Entity` property that may hold stored data. */
class EntityPropertyStoredFlowSource extends StoredFlowSource {
EntityPropertyStoredFlowSource() {
this.asExpr().(PropertyAccess).getTarget() = any(SystemDataEntity::Entity e).getAProperty()
}
}
/**
* An expression that has a type of `DbRawSqlQuery`, representing the result of an Entity Framework
* SqlQuery.
@@ -37,9 +31,7 @@ class DbDataReaderStoredFlowSource extends StoredFlowSource {
}
}
/**
* An expression that accesses a method of `DbDataReader` or a sub-class.
*/
/** An expression that accesses a method of `DbDataReader` or a sub-class. */
class DbDataReaderMethodStoredFlowSource extends StoredFlowSource {
DbDataReaderMethodStoredFlowSource() {
this.asExpr().(MethodCall).getTarget().getDeclaringType() = any(SystemDataCommon::DbDataReader dataReader
@@ -47,12 +39,15 @@ class DbDataReaderMethodStoredFlowSource extends StoredFlowSource {
}
}
/**
* An expression that accesses a property of `DbDataReader` or a sub-class.
*/
/** An expression that accesses a property of `DbDataReader` or a sub-class. */
class DbDataReaderPropertyStoredFlowSource extends StoredFlowSource {
DbDataReaderPropertyStoredFlowSource() {
this.asExpr().(PropertyAccess).getTarget().getDeclaringType() = any(SystemDataCommon::DbDataReader dataReader
).getASubType*()
}
}
/** A read of a mapped property. */
class EntityFrameworkMappedProperty extends StoredFlowSource {
EntityFrameworkMappedProperty() { this instanceof EntityFramework::StoredFlowSource }
}

View File

@@ -0,0 +1,7 @@
| EntityFramework.cs:52:18:52:24 | access to property Name | EntityFramework.cs:47:34:47:42 | "tainted" |
| EntityFramework.cs:53:18:53:34 | access to property Name | EntityFramework.cs:47:34:47:42 | "tainted" |
| EntityFrameworkCore.cs:50:18:50:28 | access to local variable taintSource | EntityFrameworkCore.cs:47:31:47:39 | "tainted" |
| EntityFrameworkCore.cs:51:18:51:46 | (...) ... | EntityFrameworkCore.cs:47:31:47:39 | "tainted" |
| EntityFrameworkCore.cs:52:18:52:42 | (...) ... | EntityFrameworkCore.cs:47:31:47:39 | "tainted" |
| EntityFrameworkCore.cs:60:18:60:24 | access to property Name | EntityFrameworkCore.cs:47:31:47:39 | "tainted" |
| EntityFrameworkCore.cs:61:18:61:34 | access to property Name | EntityFrameworkCore.cs:47:31:47:39 | "tainted" |

View File

@@ -0,0 +1,19 @@
import csharp
import semmle.code.csharp.dataflow.TaintTracking
class MyConfiguration extends TaintTracking::Configuration
{
MyConfiguration() { this = "EntityFramework dataflow" }
override predicate isSource(DataFlow::Node node) {
node.asExpr().getValue() = "tainted"
}
override predicate isSink(DataFlow::Node node) {
node.asExpr() = any(MethodCall c | c.getTarget().hasName("Sink")).getAnArgument()
}
}
from MyConfiguration config, DataFlow::Node source, DataFlow::Node sink
where config.hasFlow(source, sink)
select sink, source

View File

@@ -0,0 +1,65 @@
// semmle-extractor-options: /r:System.Data.dll /r:System.ComponentModel.Primitives.dll /r:System.ComponentModel.TypeConverter.dll ${testdir}/../../../resources/stubs/EntityFramework.cs ${testdir}/../../../resources/stubs/System.Data.cs /r:System.ComponentModel.TypeConverter.dll /r:System.Data.Common.dll
using System.Data.Entity;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Common;
namespace EFTests
{
class Person
{
public int Id { get; set; }
public string Name { get; set; }
[NotMapped]
public int Age { get; set; }
}
class MyContext : DbContext
{
DbSet<Person> person { get; set; }
void FlowSources()
{
var p = new Person();
var id = p.Id;
var name = p.Name;
var age = p.Age;
}
DbCommand command;
async void SqlSinks()
{
// System.Data.Common.DbCommand.set_CommandText
command.CommandText = "";
// System.Data.SqlClient.SqlCommand.SqlCommand
new System.Data.SqlClient.SqlCommand("");
this.Database.ExecuteSqlCommand("");
await this.Database.ExecuteSqlCommandAsync("");
}
void TestDataFlow()
{
string taintSource = "tainted";
// Tainted via database, even though technically there were no reads or writes to the database in this particular case.
var p1 = new Person { Name = taintSource };
var p2 = new Person();
Sink(p2.Name); // Tainted
Sink(new Person().Name); // Tainted
p1.Age = int.Parse(taintSource);
Sink(p2.Age); // Not tainted due to NotMappedAttribute
}
void Sink(object @object)
{
}
}
}

View File

@@ -0,0 +1,71 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
namespace EFCoreTests
{
class Person
{
public int Id { get; set; }
public string Name { get; set; }
[NotMapped]
public int Age { get; set; }
}
class MyContext : DbContext
{
DbSet<Person> person;
void FlowSources()
{
var p = new Person();
var id = p.Id; // Remote flow source
var name = p.Name; // Remote flow source
var age = p.Age; // Not a remote flow source
}
Microsoft.EntityFrameworkCore.Storage.IRawSqlCommandBuilder builder;
async void SqlExprs()
{
// Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.ExecuteSqlCommand
this.Database.ExecuteSqlCommand(""); // SqlExpr
await this.Database.ExecuteSqlCommandAsync(""); // SqlExpr
// Microsoft.EntityFrameworkCore.Storage.IRawSqlCommandBuilder.Build
builder.Build(""); // SqlExpr
// Microsoft.EntityFrameworkCore.RawSqlString
new RawSqlString(""); // SqlExpr
RawSqlString str = ""; // SqlExpr
}
void TestDataFlow()
{
var taintSource = "tainted";
var untaintedSource = "untainted";
Sink(taintSource); // Tainted
Sink(new RawSqlString(taintSource)); // Tainted
Sink((RawSqlString)taintSource); // Tainted
Sink((RawSqlString)(FormattableString)$"{taintSource}"); // Not tainted
// Tainted via database, even though technically there were no reads or writes to the database in this particular case.
var p1 = new Person { Name = taintSource };
p1.Name = untaintedSource;
var p2 = new Person();
Sink(p2.Name); // Tainted
Sink(new Person().Name); // Tainted
p1.Age = int.Parse(taintSource);
Sink(p2.Age); // Not tainted due to NotMappedAttribute
}
void Sink(object @object)
{
}
}
}

View File

@@ -0,0 +1,8 @@
| EntityFramework.cs:26:22:26:25 | access to property Id |
| EntityFramework.cs:27:24:27:29 | access to property Name |
| EntityFramework.cs:52:18:52:24 | access to property Name |
| EntityFramework.cs:53:18:53:34 | access to property Name |
| EntityFrameworkCore.cs:24:22:24:25 | access to property Id |
| EntityFrameworkCore.cs:25:24:25:29 | access to property Name |
| EntityFrameworkCore.cs:60:18:60:24 | access to property Name |
| EntityFrameworkCore.cs:61:18:61:34 | access to property Name |

View File

@@ -0,0 +1,6 @@
import csharp
import semmle.code.csharp.security.dataflow.flowsources.Stored
from StoredFlowSource source
select source

View File

@@ -0,0 +1,4 @@
| EntityFramework.cs:12:20:12:21 | Id |
| EntityFramework.cs:13:23:13:26 | Name |
| EntityFrameworkCore.cs:10:18:10:19 | Id |
| EntityFrameworkCore.cs:11:21:11:24 | Name |

View File

@@ -0,0 +1,6 @@
import csharp
import semmle.code.csharp.frameworks.EntityFramework
from EntityFramework::MappedProperty property
select property

View File

@@ -0,0 +1,11 @@
| EntityFramework.cs:36:13:36:36 | ... = ... | System.Data.Common.DbCommand.set_CommandText |
| EntityFramework.cs:39:13:39:52 | object creation of type SqlCommand | System.Data.SqlClient.SqlCommand.SqlCommand |
| EntityFramework.cs:41:13:41:47 | call to method ExecuteSqlCommand | System.Data.Entity.Database.ExecuteSqlCommand |
| EntityFramework.cs:42:19:42:58 | call to method ExecuteSqlCommandAsync | System.Data.Entity.Database.ExecuteSqlCommandAsync |
| EntityFrameworkCore.cs:34:13:34:47 | call to method ExecuteSqlCommand | Microsoft.EntityFrameworkCore.RelationalDatabaseFacaseExtensions.ExecuteSqlCommand |
| EntityFrameworkCore.cs:35:19:35:58 | call to method ExecuteSqlCommandAsync | Microsoft.EntityFrameworkCore.RelationalDatabaseFacaseExtensions.ExecuteSqlCommandAsync |
| EntityFrameworkCore.cs:38:13:38:29 | call to method Build | Microsoft.EntityFrameworkCore.Storage.IRawSqlCommandBuilder.Build |
| EntityFrameworkCore.cs:41:13:41:32 | object creation of type RawSqlString | Microsoft.EntityFrameworkCore.RawSqlString.RawSqlString |
| EntityFrameworkCore.cs:42:32:42:33 | call to operator implicit conversion | Microsoft.EntityFrameworkCore.RawSqlString.implicit conversion |
| EntityFrameworkCore.cs:51:18:51:46 | object creation of type RawSqlString | Microsoft.EntityFrameworkCore.RawSqlString.RawSqlString |
| EntityFrameworkCore.cs:52:18:52:42 | call to operator implicit conversion | Microsoft.EntityFrameworkCore.RawSqlString.implicit conversion |

View File

@@ -0,0 +1,5 @@
import csharp
import semmle.code.csharp.frameworks.Sql
from SqlExpr expr
select expr, expr.(Call).getTarget().getQualifiedName()

View File

@@ -32,14 +32,14 @@ public class BloggingContext : DbContext
DbRawSqlQuery<Blog> blogs = Database.SqlQuery<Blog>("SELECT * FROM Blogs");
foreach (var blog in blogs)
{
// This will be a sink because it is an access of an entity property
// This will be a source because it is an access of an entity property
Console.WriteLine(blog.Name);
}
DbRawSqlQuery<string> blogNames = Database.SqlQuery<string>("SELECT Name FROM Blogs");
foreach (var blogName in blogNames)
{
// This will be a sink because it is returned from an SqlQuery
// This will be a source because it is returned from an SqlQuery
Console.WriteLine(blogName);
}
}

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Threading.Tasks;
using System;
namespace System.Data.Entity
{
@@ -16,6 +18,7 @@ namespace System.Data.Entity
{
public int ExecuteSqlQuery(string sql, params object[] parameters) => 0;
public int ExecuteSqlCommand(string sql, params object[] parameters) => 0;
public async Task ExecuteSqlCommandAsync(string sql, params object[] parameters) => throw null;
public Infrastructure.DbRawSqlQuery<T> SqlQuery<T>(string sql, params object[] parameters) => null;
}
@@ -41,3 +44,52 @@ namespace System.Data.Entity.Infrastructure
IList IListSource.GetList() => null;
}
}
namespace Microsoft.EntityFrameworkCore
{
public class DbSet<T>
{
}
public class DbContext : IDisposable
{
public void Dispose() { }
public virtual Infrastructure.DatabaseFacade Database => null;
// public Infrastructure.DbRawSqlQuery<TElement> SqlQuery<TElement>(string sql, params object[] parameters) => null;
}
namespace Infrastructure
{
public class DatabaseFacade
{
}
}
public static class RelationalDatabaseFacaseExtensions
{
public static void ExecuteSqlCommand(this Infrastructure.DatabaseFacade db, string sql, params object[] parameters) {}
public static Task ExecuteSqlCommandAsync(this Infrastructure.DatabaseFacade db, string sql, params object[] parameters) => throw null;
}
struct RawSqlString
{
public RawSqlString(string str) { }
public static implicit operator Microsoft.EntityFrameworkCore.RawSqlString (FormattableString fs) => throw null;
public static implicit operator Microsoft.EntityFrameworkCore.RawSqlString (string s) => throw null;
}
}
namespace System.ComponentModel.DataAnnotations.Schema
{
class NotMappedAttribute : Attribute
{
}
}
namespace Microsoft.EntityFrameworkCore.Storage
{
interface IRawSqlCommandBuilder
{
void Build(string sql);
}
}

View File

@@ -71,6 +71,7 @@ namespace System.Data
IDataReader ExecuteReader();
CommandType CommandType { get; set; }
IDataParameterCollection Parameters { get; set; }
string CommandText { get; set; }
}
public interface IDataReader
@@ -117,13 +118,14 @@ namespace System.Data.Common
public virtual string GetString(int i) => "";
}
public class DbCommand : IDbCommand, IDisposable
public abstract class DbCommand : IDbCommand, IDisposable
{
public DbDataReader ExecuteReader() => null;
public CommandType CommandType { get; set; }
public IDataParameterCollection Parameters { get; set; }
IDataReader IDbCommand.ExecuteReader() => null;
public void Dispose() { }
public string CommandText { get; set; }
}
public class DbDataAdapter : IDataAdapter, IDbDataAdapter