| description | Guide for implementing C# security queries in CodeQL |
|---|
This document provides specific, actionable instructions for implementing C# security queries in CodeQL, focusing on common vulnerability patterns like SQL injection, XSS, SSRF, and encoding-related issues.
For Security Query Implementation (.ql/.qll files):
// Standard security query imports - USE THESE FOR ALL C# SECURITY QUERIES
import csharp
import semmle.code.csharp.dataflow.DataFlow
import semmle.code.csharp.dataflow.TaintTracking
import semmle.code.csharp.security.dataflow.SqlInjectionQuery
import semmle.code.csharp.security.dataflow.flowsinks.Html
import semmle.code.csharp.security.dataflow.UrlRedirectQuery
import semmle.code.csharp.security.Sanitizers
// Optional: Include path graph support for path-problem queries
import DataFlow::PathGraphFor Framework-Specific Queries:
// .NET Framework-specific imports as needed
import semmle.code.csharp.frameworks.System
import semmle.code.csharp.frameworks.system.Net
import semmle.code.csharp.frameworks.system.Web
import semmle.code.csharp.frameworks.system.web.UI
import semmle.code.csharp.frameworks.system.DataCommon patterns for detecting user-controlled input:
// ASP.NET request parameters
class AspNetRequestSource extends DataFlow::Node {
AspNetRequestSource() {
exists(PropertyCall pc |
pc = this.asExpr() and
pc.getTarget().getName() in ["QueryString", "Form", "Headers", "Cookies"] and
pc.getQualifier().(PropertyCall).getTarget().getName() = "Request"
)
}
}
// Generic user input patterns
class UserControlledData extends DataFlow::Node {
UserControlledData() {
// HTTP request data
exists(MethodCall mc |
mc = this.asExpr() and
mc.getTarget().getName() in ["GetValues", "Get"] and
mc.getQualifier() instanceof AspNetRequestSource
)
or
// Console input
exists(MethodCall mc |
mc = this.asExpr() and
mc.getTarget().getDeclaringType().hasName("Console") and
mc.getTarget().getName() in ["ReadLine", "Read"]
)
}
}Common patterns for SQL injection vulnerability detection:
// SQL command construction sinks
class SqlCommandSink extends DataFlow::Node {
SqlCommandSink() {
// SqlCommand constructor or CommandText property
exists(ObjectCreation oc |
oc = this.asExpr() and
oc.getType().getName().matches("%SqlCommand%") and
this.asExpr() = oc.getArgument(0)
)
or
exists(PropertyCall pc |
pc.getTarget().getName() = "CommandText" and
this.asExpr() = pc.getArgument(0)
)
or
// SqlDataAdapter constructor
exists(ObjectCreation oc |
oc = this.asExpr() and
oc.getType().getName().matches("%SqlDataAdapter%") and
this.asExpr() = oc.getArgument(0)
)
}
}Common patterns for XSS vulnerability detection:
// HTML output sinks
class HtmlOutputSink extends DataFlow::Node {
HtmlOutputSink() {
// Response.Write calls
exists(MethodCall mc |
mc = this.asExpr().getParent*() and
mc.getTarget().getName() in ["Write", "WriteAsync"] and
mc.getTarget().getDeclaringType().getName().matches("%HttpResponse%") and
this.asExpr() = mc.getArgument(0)
)
or
// Label.Text assignments in Web Forms
exists(AssignExpr ae |
ae.getLValue().(PropertyCall).getTarget().getName() = "Text" and
ae.getLValue().(PropertyCall).getTarget().getDeclaringType().getName().matches("%Label%") and
this.asExpr() = ae.getRValue()
)
or
// HTML element attribute assignments
exists(MethodCall mc |
mc.getTarget().getName() = "SetAttribute" and
this.asExpr() = mc.getArgument(1)
)
}
}Common patterns for URL redirection vulnerability detection:
// URL redirect sinks
class UrlRedirectSink extends DataFlow::Node {
UrlRedirectSink() {
// Response.Redirect calls
exists(MethodCall mc |
mc.getTarget().getName() in ["Redirect", "RedirectPermanent"] and
mc.getTarget().getDeclaringType().getName().matches("%HttpResponse%") and
this.asExpr() = mc.getArgument(0)
)
or
// Location header assignments
exists(AssignExpr ae |
ae.getLValue().(PropertyCall).getTarget().getName() = "Location" and
this.asExpr() = ae.getRValue()
)
}
}- SQL Injection - Database query construction with user input
- Cross-Site Scripting (XSS) - HTML output with unescaped user input
- URL Redirection - Redirects to user-controlled URLs
- Command Injection - OS command execution with user input
- Path Traversal - File operations with user-controlled paths
- Inappropriate Encoding - Wrong encoding methods for context
- Authentication Bypass - Missing authentication checks
- Authorization Issues - Insufficient access controls
Encoding Methods Detection:
// .NET encoding methods
class EncodingMethod extends MethodCall {
string getEncodingType() {
// HTML encoding methods
this.getTarget().getDeclaringType().getName() in ["HttpUtility", "WebUtility", "HttpServerUtility"] and
this.getTarget().getName().matches("%HtmlEncode%") and
result = "html"
or
// URL encoding methods
this.getTarget().getDeclaringType().getName() in ["HttpUtility", "WebUtility", "HttpServerUtility"] and
this.getTarget().getName().matches("%UrlEncode%") and
result = "url"
or
// JavaScript encoding methods
this.getTarget().getDeclaringType().getName() = "HttpUtility" and
this.getTarget().getName().matches("%JavaScriptEncode%") and
result = "javascript"
or
// Custom encoding (potentially inappropriate)
this.getTarget().fromSource() and
this.getTarget().getName().toLowerCase().matches("%encode%") and
result = "custom"
}
}Inappropriate Encoding Detection Pattern:
// Configuration for inappropriate encoding detection
private class InappropriateEncodingConfiguration extends TaintTracking::Configuration {
InappropriateEncodingConfiguration() { this = "InappropriateEncodingConfiguration" }
override predicate isSource(DataFlow::Node source) {
// Encoded values that might be inappropriate for their context
source.asExpr() instanceof EncodingMethod
}
override predicate isSink(DataFlow::Node sink) {
// Sinks that require specific encoding types
sink instanceof SqlCommandSink or
sink instanceof HtmlOutputSink or
sink instanceof UrlRedirectSink
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
// String concatenation and formatting preserve taint
exists(AddExpr add |
add.getAnOperand() = node1.asExpr() and
node2.asExpr() = add
)
or
exists(MethodCall format |
format.getTarget().getName() = "Format" and
format.getTarget().getDeclaringType().getName() = "String" and
format.getAnArgument() = node1.asExpr() and
node2.asExpr() = format
)
}
}Create a .qhelp file describing the vulnerability:
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
<qhelp>
<overview>
<p>
[Description of the security vulnerability and its impact]
</p>
</overview>
<recommendation>
<p>
[Recommendations for fixing the vulnerability]
</p>
</recommendation>
<example>
<p>
[Example code showing vulnerable and safe patterns]
</p>
</example>
<references>
<li>OWASP: <a href="[URL]">[Reference title]</a></li>
</references>
</qhelp>Create comprehensive test cases in .cs files:
using System;
using System.Data.SqlClient;
using System.Web;
public class SecurityTest
{
public void VulnerableMethod(string userInput)
{
// BAD: Vulnerable pattern
var query = "SELECT * FROM users WHERE name = '" + userInput + "'";
var cmd = new SqlCommand(query, connection);
}
public void SafeMethod(string userInput)
{
// GOOD: Safe pattern
var query = "SELECT * FROM users WHERE name = @name";
var cmd = new SqlCommand(query, connection);
cmd.Parameters.AddWithValue("@name", userInput);
}
}/**
* @name [Human-readable vulnerability name]
* @description [Description of the vulnerability and its impact]
* @kind path-problem
* @problem.severity [error|warning|recommendation]
* @security-severity [0.0-10.0]
* @precision [low|medium|high|very-high]
* @id [language]/[cwe-category]/[specific-id]
* @tags security
* external/cwe/cwe-[number]
*/
import csharp
import VulnerabilityNameQuery
import DataFlow::PathGraph
from VulnerabilityNameFlow::PathNode source, VulnerabilityNameFlow::PathNode sink
where VulnerabilityNameFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "[Alert message with $@ placeholder]",
source.getNode(), "[source description]"// NON_COMPLIANT: [Vulnerability description]
public void VulnerableMethod(string userInput)
{
var source = GetUserInput(); // Source: user input
VulnerableFunction(source); // Sink: dangerous operation
}
// COMPLIANT: [Safe alternative description]
public void SafeMethod(string userInput)
{
var userInput = GetUserInput();
var sanitized = SanitizeInput(userInput); // Sanitizer
SafeFunction(sanitized); // Safe: input sanitized
}| file.cs:line:col:line:col | Alert message with $@ | file.cs:source_line:source_col:source_line:source_col | source description |
// ASP.NET specific sources and sinks
class AspNetSource extends DataFlow::Node {
AspNetSource() {
// Request parameters, headers, cookies, etc.
exists(PropertyCall pc |
pc = this.asExpr() and
pc.getTarget().getDeclaringType().getName() = "HttpRequest" and
pc.getTarget().getName() in ["QueryString", "Form", "Headers", "Cookies"]
)
}
}
class AspNetSink extends DataFlow::Node {
AspNetSink() {
// Response output, redirects, etc.
exists(MethodCall mc |
mc.getTarget().getDeclaringType().getName() = "HttpResponse" and
mc.getTarget().getName() in ["Write", "Redirect"] and
this.asExpr() = mc.getArgument(0)
)
}
}// Entity Framework specific patterns
class EntityFrameworkSink extends DataFlow::Node {
EntityFrameworkSink() {
// Raw SQL execution
exists(MethodCall mc |
mc.getTarget().getName() in ["ExecuteSqlCommand", "FromSql", "ExecuteSqlRaw"] and
this.asExpr() = mc.getArgument(0)
)
}
}- Use specific type restrictions to limit analysis scope
- Leverage existing security libraries rather than reimplementing
- Use
isAdditionalTaintStepfor custom propagation rules - Consider using
isBarrierto stop false positive flows
// Use specific type constraints
predicate isRelevantCall(MethodCall mc) {
mc.getTarget().getDeclaringType().getNamespace().getName().matches("System.%") and
mc.getTarget().getName().matches("%Encode%")
}
// Limit scope to security-relevant code
predicate isSecurityRelevantFile(File f) {
f.getRelativePath().matches("%Controller%") or
f.getRelativePath().matches("%Service%") or
f.getRelativePath().matches("%Handler%")
}For C# security query development and testing: