Skip to main content
Version: 2026.04.0

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.

Cross-file taint chain (TaintChain)

The scanner traces the full call path from the user-controlled source to the dangerous sink and exposes it as a structured TaintChain property on SecurityFinding. The chain is always non-empty for PS-CF-* findings — when the sink is reached directly in the entry method with no intermediate hops, a single entry describes the direct sink location.

tip

TaintChain was added in v2.4.9. Earlier versions embedded the chain as plain text inside Description only.

JSON output

When you run primus-scan ... --json, each PS-CF-* finding includes a TaintChain array:

{
"RuleId": "PS-CF-002",
"Title": "Cross-File XSS: taint from 'name' reaches 'Content'",
"Severity": 3,
"TaintChain": [
"Controllers/UsersController.cs:12 → GetUser(id)",
"Services/UserService.cs:34 → BuildResponse(userId)",
"Controllers/UsersController.cs:39 → Content(name) [sink]"
]
}

Each element is a string formatted as "FilePath:line → MethodName(param)". The final entry always points to where the tainted value reaches the dangerous sink.

VS Code extension

In the IDE, open the rule explainer for any cross-file finding (click a finding in the sidebar → Explain Rule). The panel shows a visual Cross-File Taint Chain section with each hop rendered as a card you can click to jump to that file and line.

SecurityFinding reference

public class SecurityFinding
{
// ... existing fields ...

/// Cross-file taint propagation chain.
/// Only populated for PS-CF-* findings.
/// Each element: "File.cs:line → MethodName(param)"
public List<string> TaintChain { get; set; } = new();
}

Suppressions

To suppress a cross-file finding inline, add // primus-nosec PS-CF-002 on the sink line (the last entry in the chain). You can also add it to .primus-suppressions.json using the finding's UUID.