Taint Flow Analysis
Primus Security uses two complementary taint engines:
| Engine | Scope | Confidence |
|---|---|---|
| Single-file Roslyn analyzers | Within one .cs file | 0.90 |
| Cross-file taint propagator | Across Controller → Service → Repository | 0.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
| Vulnerability | Sink methods |
|---|---|
| SQL Injection | ExecuteNonQuery, ExecuteSqlRaw, FromSqlRaw, Query (Dapper) |
| XSS | Html.Raw, HtmlString, response Write |
| SSRF | HttpClient.GetAsync/PostAsync/SendAsync, new Uri() |
| Path Traversal | File.ReadAllText, File.Open, Path.Combine |
| Command Injection | Process.Start, new ProcessStartInfo() |
| Open Redirect | Controller.Redirect, LocalRedirect |
| Sensitive Data Logging | ILogger.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
| Engine | Confidence | Reason |
|---|---|---|
| Single-file Roslyn | 0.90 | Exact AST + SemanticModel |
| Cross-file propagator | 0.80 | Call 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
| Source | Pattern |
|---|---|
[FromQuery] / [FromBody] / [FromRoute] | ASP.NET model binding attributes |
Request.Query / Request.Form / Request.Headers | HttpContext properties |
| Action method parameters | MVC route values |
Console.ReadLine() | Console input |
Supported sinks (sample)
| Sink | Rule |
|---|---|
| SQL concatenation | PS-SEC-001 |
Process.Start | PS-SEC-006 |
File.Open / Path.Combine | PS-SEC-007 |
HttpClient.GetAsync | PS-SEC-009 |
Html.Raw / HtmlString | PS-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:
| Source | Pattern |
|---|---|
Request.Query / Request.QueryString / Request.Form | HTTP query/form parameters |
RouteData.Values / route values | URL route segments |
Controller action parameters (string id, int value) | MVC action method parameters |
Console.ReadLine() | Console input |
JsonSerializer.Deserialize / ReadFromJsonAsync | Deserialized HTTP body |
Environment.GetEnvironmentVariable() | Environment variables |
File.ReadAllText / StreamReader | File content |
Supported sinks
| Sink | Rule | Severity |
|---|---|---|
SQL concatenation / CommandText = | PS-SEC-001 | High |
SQL via string interpolation ($"SELECT...{var}") | PS-SEC-001 | High |
Process.Start / ProcessStartInfo | PS-SEC-004 | High |
File.Open / File.Read / Path.Combine | PS-SEC-007 | High |
Redirect() / Response.Redirect() | PS-SEC-010 | Medium |
| Logger methods with tainted args | PS-SEC-006 | Medium |
HttpClient.GetAsync / SendAsync with tainted URL | PS-SEC-009 | High |
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.