Skip to main content

Taint Flow Analysis

Primus Security uses two complementary taint engines:

EngineScopeConfidence
Single-file Roslyn analyzersWithin one .cs file0.90
Cross-file taint propagatorAcross Controller → Service → Repository0.80

Both are active by default inside ScanAsync(). No configuration required.

Why taint flow matters

Simple pattern matching flags any SQL string — including hard-coded ones that are perfectly safe. Taint flow checks: "does user-controlled input actually reach this dangerous sink?"

// Pattern-only: FLAGGED (SQL string found)
// Taint flow: CLEAN (no user input reaches it)
var sql = "SELECT * FROM Products WHERE Category = 'Electronics'";
db.Execute(sql);

// Both flag this — taint proven, confidence 0.90
var category = Request.Query["category"];
var sql = $"SELECT * FROM Products WHERE Category = '{category}'";
db.Execute(sql);

Cross-file taint analysis (Phase 1)

The most common real-world vulnerability pattern spans multiple files:

// UserController.cs — receives HTTP input, calls service
public IActionResult Get(string id)
{
return Ok(_service.GetUser(id)); // id is tainted here
}

// UserService.cs — passes to repository
public User GetUser(string id) => _repo.Find(id);

// UserRepository.cs — builds raw SQL (SINK)
public User Find(string id) =>
db.Execute($"SELECT * FROM Users WHERE Id = '{id}'");
// ↑ SQL injection — single-file analysis misses this entirely

The MethodCallGraph builds a directed call graph across all .cs files in the compilation. The CrossFileTaintPropagator then follows tainted arguments through the graph up to 5 hops (configurable), emitting a CrossFileTaintFlowFinding with the full call chain as evidence.

Cross-file findings use rule IDs prefixed PS-CF- (e.g. PS-CF-001 for cross-file SQL injection) to distinguish them from single-file findings.

What's detected cross-file

VulnerabilitySink methods
SQL InjectionExecuteNonQuery, ExecuteSqlRaw, FromSqlRaw, Query (Dapper)
XSSHtml.Raw, HtmlString, response Write
SSRFHttpClient.GetAsync/PostAsync/SendAsync, new Uri()
Path TraversalFile.ReadAllText, File.Open, Path.Combine
Command InjectionProcess.Start, new ProcessStartInfo()
Open RedirectController.Redirect, LocalRedirect
Sensitive Data LoggingILogger.Log*

Reading cross-file findings

var result = await scanner.ScanAsync("/path/to/project");

var crossFileFindings = result.Findings
.Where(f => f.RuleId?.StartsWith("PS-CF-") == true)
.ToList();

foreach (var finding in crossFileFindings)
{
Console.WriteLine($"[{finding.RuleId}] {finding.Title}");
Console.WriteLine($"Sink: {finding.FilePath}:{finding.Line}");
Console.WriteLine($"Chain: {finding.Description}");
// "Cross-file SQL Injection (80% confidence).
// Chain: UserController.cs:24 (Get) → UserService.cs:18 (GetUser)
// → UserRepository.cs:31 (Find)"
}

Confidence levels

EngineConfidenceReason
Single-file Roslyn0.90Exact AST + SemanticModel
Cross-file propagator0.80Call graph construction uses approximation

Lower confidence means slightly more potential false positives. Cross-file findings should be reviewed before suppressing.

Single-file taint (per-analyzer)

The 60 Roslyn analyzers each run their own TaintEngine instance per-compilation. Sources, propagation, and sink detection within a single file achieve 0.90 confidence.

Supported sources

SourcePattern
[FromQuery] / [FromBody] / [FromRoute]ASP.NET model binding attributes
Request.Query / Request.Form / Request.HeadersHttpContext properties
Action method parametersMVC route values
Console.ReadLine()Console input

Supported sinks (sample)

SinkRule
SQL concatenationPS-SEC-001
Process.StartPS-SEC-006
File.Open / Path.CombinePS-SEC-007
HttpClient.GetAsyncPS-SEC-009
Html.Raw / HtmlStringPS-SEC-002

For the full list see the rule catalog.

Suppressing taint findings

// Inline — single line
var sql = $"SELECT * FROM Products WHERE Id = {id}"; // primus-nosec

// File-level — .primusignore in project root
**/Migrations/*.cs
**/Seed/*.cs

For persistent cross-run suppression see Suppression Store.

Why taint flow matters

Simple pattern matching flags any SQL string — including hard-coded ones that are perfectly safe. Taint flow checks: "does user-controlled input actually reach this dangerous sink?"

// Pattern-only scanner: FLAGGED (SQL string found)
// Taint flow scanner: CLEAN (no user input reaches it)
var sql = "SELECT * FROM Products WHERE Category = 'Electronics'";
db.Execute(sql);

// Pattern-only scanner: FLAGGED
// Taint flow scanner: FLAGGED with HIGH confidence (taint proven to reach sink)
var category = Request.Query["category"];
var sql = $"SELECT * FROM Products WHERE Category = '{category}'";
db.Execute(sql);

Taint flow findings carry Confidence: 0.90 vs 0.70–0.85 for pattern-only.

Usage

using PrimusSaaS.Security.Heuristics;

var analyzer = new TaintFlowAnalyzer();
var code = File.ReadAllText("Controllers/ProductsController.cs");

var findings = analyzer.Analyze(code);

foreach (var finding in findings)
{
Console.WriteLine(finding.Message);
// "SQL Injection: 'category' flows from user input (line 12) to dangerous sink (line 15)."

Console.WriteLine($"Source: {finding.SourceSnippet} (line {finding.SourceLine})");
Console.WriteLine($"Sink: {finding.SinkSnippet} (line {finding.SinkLine})");
Console.WriteLine($"Confidence: {finding.Confidence:P0}");
}

Supported sources

The analyzer recognises these user-controlled input sources in C# / ASP.NET:

SourcePattern
Request.Query / Request.QueryString / Request.FormHTTP query/form parameters
RouteData.Values / route valuesURL route segments
Controller action parameters (string id, int value)MVC action method parameters
Console.ReadLine()Console input
JsonSerializer.Deserialize / ReadFromJsonAsyncDeserialized HTTP body
Environment.GetEnvironmentVariable()Environment variables
File.ReadAllText / StreamReaderFile content

Supported sinks

SinkRuleSeverity
SQL concatenation / CommandText =PS-SEC-001High
SQL via string interpolation ($"SELECT...{var}")PS-SEC-001High
Process.Start / ProcessStartInfoPS-SEC-004High
File.Open / File.Read / Path.CombinePS-SEC-007High
Redirect() / Response.Redirect()PS-SEC-010Medium
Logger methods with tainted argsPS-SEC-006Medium
HttpClient.GetAsync / SendAsync with tainted URLPS-SEC-009High

TaintFlowFinding reference

public sealed class TaintFlowFinding
{
public string VulnerabilityType { get; } // e.g. "SQL Injection"
public ThreatSeverity Severity { get; }
public string RuleId { get; } // e.g. "PS-SEC-001"
public double Confidence { get; } // 0.90 when taint is proven

public string SourceDescription { get; } // Human-readable taint source
public int SourceLine { get; }
public string SourceSnippet { get; }

public int SinkLine { get; }
public string SinkSnippet { get; }
public string TaintedVariable { get; }

public string Message { get; }
// "SQL Injection: 'id' flows from user input (line 8) to dangerous sink (line 12)."
}

Integration with the main scanner

The SecurityScanner runs taint flow analysis as part of ScanAsync() automatically — you do not need to call TaintFlowAnalyzer directly in most cases. It appears in findings with the Source = "TaintFlow" property and higher confidence scores than pattern-only findings for the same rule.