/ Update

8 min

中文

C Sharp 代码审计 - 漏洞Sink点

views | comments

1. SQL注入#

Sink点#

  • SqlCommand.ExecuteNonQuery()
  • SqlCommand.ExecuteReader()
  • SqlCommand.ExecuteScalar()
  • SqlDataAdapter.Fill()
  • DbContext.Database.ExecuteSql()
  • DbContext.Database.SqlQuery()
  • Dapper connection.Query()
  • FromSqlRaw()
  • FromSqlInterpolated()

审计检查项#

  • 是否使用参数化查询(占位符@parameter)
  • SQL字符串是否通过拼接构造
  • 用户输入是否直接进入SQL
  • 参数是否通过Parameters.AddWithValue()绑定
  • 所有用户输入是否都被参数化处理
  • EF Core FromSql()FromSqlRaw()区别
  • 特殊子句(ORDER BY)是否有白名单
  • 存储过程参数是否验证

风险代码模式#

// 模式1:直接拼接
string username = Request.QueryString["username"];
string sql = "SELECT * FROM Users WHERE Name = '" + username + "'";
var cmd = new SqlCommand(sql, connection);
var result = cmd.ExecuteReader();

csharp

// 模式2:EF Core FromSqlRaw使用字符串插值
string filter = Request.QueryString["filter"];
var users = context.Users
    .FromSqlRaw($"SELECT * FROM Users WHERE Name = {filter}")
    .ToList();

csharp

// 模式3:ORM框架拼接SQL
var users = db.Queryable<User>()
    .Select(x => SqlFunc.MappingColumn("SELECT * FROM Users WHERE Name = '" + userInput + "'"))
    .ToList();

csharp

安全实现#

// 方案1:参数化查询
string username = Request.QueryString["username"];
string sql = "SELECT * FROM Users WHERE Name = @username";
using (SqlCommand cmd = new SqlCommand(sql, connection))
{
    cmd.Parameters.AddWithValue("@username", username);
    var reader = cmd.ExecuteReader();
}

csharp

// 方案2:EF Core参数化
var username = Request.QueryString["username"];
var users = await context.Users
    .FromSql($"SELECT * FROM Users WHERE Name = {username}")
    .ToListAsync();

csharp

// 方案3:LINQ避免手写SQL
var username = Request.QueryString["username"];
var users = context.Users
    .Where(u => u.Name == username)
    .ToList();

csharp

// 方案4:ORDER BY白名单
string orderBy = Request.QueryString["orderBy"] ?? "id";
var allowedFields = new[] { "id", "name", "email", "createdAt" };
if (!allowedFields.Contains(orderBy))
    throw new ArgumentException("Invalid field");
var sql = "SELECT * FROM Users ORDER BY " + orderBy;

csharp


2. 命令执行#

Sink点#

  • Process.Start()
  • ProcessStartInfo.FileName
  • ProcessStartInfo.Arguments
  • ProcessStartInfo.UserName
  • ProcessStartInfo.Password

审计检查项#

  • 用户输入是否直接作为FileName
  • Arguments是否包含用户输入
  • UseShellExecute是否为true
  • 是否使用了绝对路径
  • 是否实现了命令白名单
  • 参数是否进行了验证

风险代码模式#

// 模式1:用户输入作为命令
string cmd = Request.QueryString["cmd"];
Process.Start(cmd);

csharp

// 模式2:通过shell执行
string hostname = Request.QueryString["hostname"];
Process.Start("cmd.exe", "/c ipconfig " + hostname);

csharp

// 模式3:ProcessStartInfo配置不当
var psi = new ProcessStartInfo
{
    FileName = "powershell.exe",
    Arguments = "-Command " + userCmd,
    UseShellExecute = true  // 通过shell执行,更危险
};
Process.Start(psi);

csharp

安全实现#

// 方案:命令白名单
var userCmd = Request.QueryString["cmd"];
var allowedCommands = new[] { "whoami", "date", "pwd" };

if (!allowedCommands.Contains(userCmd))
    throw new ArgumentException("Command not allowed");

Process.Start(userCmd);

csharp

// 方案:参数白名单
var hostname = Request.QueryString["hostname"];
var allowedHosts = new[] { "google.com", "github.com" };

if (!allowedHosts.Contains(hostname))
    throw new ArgumentException("Host not allowed");

var psi = new ProcessStartInfo
{
    FileName = "/bin/ping",
    Arguments = $"-c 4 {hostname}",
    UseShellExecute = false,
    RedirectStandardOutput = true
};

using (var process = Process.Start(psi))
{
    var output = process.StandardOutput.ReadToEnd();
}

csharp


3. 文件上传和任意文件写入#

Sink点#

  • File.SaveAs()
  • File.WriteAllBytes()
  • File.WriteAllText()
  • FileStream.Write()
  • File.Create()
  • StreamWriter.Write()
  • Path.Combine()

审计检查项#

  • 扩展名是否进行白名单验证
  • 是否防止了.aspx.ashx等可执行文件
  • MIME类型是否经过验证
  • 文件名是否包含路径分隔符
  • 是否使用Path.GetFileName()清理
  • 是否生成了新文件名
  • 保存路径是否在预期目录内
  • 最终路径是否被规范化验证

风险代码模式#

// 模式1:直接使用上传文件名
if (Request.Files.Count > 0)
{
    var file = Request.Files[0];
    file.SaveAs(Path.Combine(Server.MapPath("~/uploads"), file.FileName));
}

csharp

// 模式2:仅检查扩展名
if (Path.GetExtension(file.FileName).ToLower() == ".jpg")
{
    file.SaveAs("uploads/" + file.FileName);
}

csharp

// 模式3:未验证保存路径
var uploadPath = "uploads/" + file.FileName;
file.SaveAs(uploadPath);

csharp

安全实现#

public ActionResult Upload(HttpPostedFileBase file)
{
    if (file == null || file.ContentLength == 0)
        return BadRequest("No file uploaded");

    const int maxFileSize = 5 * 1024 * 1024;
    if (file.ContentLength > maxFileSize)
        return BadRequest("File too large");

    var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
    var fileExtension = Path.GetExtension(file.FileName).ToLower();
    if (!allowedExtensions.Contains(fileExtension))
        return BadRequest("File type not allowed");

    var allowedMimes = new[] { "image/jpeg", "image/png", "image/gif" };
    if (!allowedMimes.Contains(file.ContentType))
        return BadRequest("Invalid MIME type");

    string safeFileName = Guid.NewGuid().ToString() + fileExtension;

    string uploadDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads");
    if (!Directory.Exists(uploadDir))
        Directory.CreateDirectory(uploadDir);

    string fullPath = Path.Combine(uploadDir, safeFileName);
    string canonicalPath = Path.GetFullPath(fullPath);
    string canonicalDir = Path.GetFullPath(uploadDir);

    if (!canonicalPath.StartsWith(canonicalDir + Path.DirectorySeparatorChar) &&
        canonicalPath != canonicalDir)
        return BadRequest("Invalid file path");

    file.SaveAs(fullPath);
    File.SetAttributes(fullPath, FileAttributes.Normal);

    return Ok(new { fileName = safeFileName });
}

csharp


4. 反序列化漏洞#

Sink点#

  • BinaryFormatter.Deserialize()
  • SoapFormatter.Deserialize()
  • NetDataContractSerializer.ReadObject()
  • LosFormatter.Deserialize()
  • ObjectStateFormatter.Deserialize()
  • JsonConvert.DeserializeObject()

审计检查项#

  • 是否对不可信输入执行反序列化
  • 数据来源是否可信(HTTP、Cookie)
  • 是否实现了类白名单
  • JsonConvertTypeNameHandling配置
  • 是否存在自定义readObject方法
  • 是否认识ViewState风险

风险代码模式#

// 模式1:直接反序列化用户输入
byte[] data = Convert.FromBase64String(Request.QueryString["data"]);
var formatter = new BinaryFormatter();
object obj = formatter.Deserialize(new MemoryStream(data));

csharp

// 模式2:LosFormatter反序列化
string viewState = Request.Form["__VIEWSTATE"];
var formatter = new LosFormatter();
object obj = formatter.Deserialize(viewState);

csharp

// 模式3:JsonConvert不安全配置
var settings = new JsonSerializerSettings 
{
    TypeNameHandling = TypeNameHandling.All
};
var obj = JsonConvert.DeserializeObject(userInput, settings);

csharp

安全实现#

// 方案1:使用安全的JsonConvert配置
var settings = new JsonSerializerSettings 
{
    TypeNameHandling = TypeNameHandling.None
};
var user = JsonConvert.DeserializeObject<User>(userInput, settings);

csharp

// 方案2:使用System.Text.Json
var options = new JsonSerializerOptions 
{
    TypeInfoResolver = null
};
var user = JsonSerializer.Deserialize<User>(userInput, options);

csharp

// 方案3:ViewState安全配置
// web.config中:
// <pages enableViewStateMac="true" viewStateEncryptionMode="Always" />

csharp


5. 任意文件读取#

Sink点#

  • File.ReadAllBytes()
  • File.ReadAllText()
  • Response.WriteFile()
  • Response.TransmitFile()
  • FileStream构造函数
  • StreamReader构造函数

审计检查项#

  • 用户参数是否直接作为文件路径
  • 是否使用Path.GetFileName()清理
  • 是否使用Path.GetFullPath()规范化
  • 最终路径是否在允许目录内
  • 是否实现了文件白名单

安全实现#

public FileResult Download(string filename)
{
    string safeName = Path.GetFileName(filename);
    string baseDir = Path.GetFullPath("~/Files/");
    string filePath = Path.Combine(baseDir, safeName);
    string canonicalPath = Path.GetFullPath(filePath);
    
    if (!canonicalPath.StartsWith(baseDir))
        return HttpNotFound();
    
    if (!System.IO.File.Exists(canonicalPath))
        return HttpNotFound();
    
    return File(System.IO.File.ReadAllBytes(canonicalPath), 
                "application/octet-stream", 
                safeName);
}

csharp


6. 路径遍历#

Sink点#

  • 所有文件操作

审计检查项#

  • 路径是否包含.../等特殊序列
  • 是否使用Path.GetFullPath()规范化
  • 规范化后路径是否在基础目录内

安全实现#

public string ValidatePath(string userInput)
{
    string baseDir = Path.GetFullPath("data/");
    string fullPath = Path.Combine(baseDir, userInput);
    string canonicalPath = Path.GetFullPath(fullPath);
    
    if (!canonicalPath.StartsWith(baseDir))
        throw new ArgumentException("Path traversal detected");
    
    return canonicalPath;
}

csharp


7. XXE (XML External Entity)#

Sink点#

  • XmlDocument.LoadXml()
  • XmlDocument.Load()
  • XmlReader.Create()
  • DataSet.ReadXml()

审计检查项#

  • 是否对不可信XML进行解析
  • DTD处理是否禁用
  • XmlResolver是否为null
  • 是否设置XIncludeAware=false

安全实现#

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null
};
using (XmlReader reader = XmlReader.Create(xmlStream, settings))
{
    var doc = new XmlDocument();
    doc.Load(reader);
}

csharp


8. SSRF (Server-Side Request Forgery)#

Sink点#

  • WebClient.DownloadString()
  • WebClient.DownloadData()
  • HttpClient.GetAsync()
  • HttpClient.PostAsync()
  • WebRequest.Create()

审计检查项#

  • 是否允许用户指定URL
  • 是否检测内部地址(127.0.0.1、localhost)
  • 是否限制了协议(仅HTTP/HTTPS)
  • 是否防止了DNS重绑定
  • 域名是否在白名单内

安全实现#

public async Task<string> FetchUrlSafely(string url)
{
    if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
        throw new ArgumentException("Invalid URL");
    
    if (uri.Scheme != "http" && uri.Scheme != "https")
        throw new ArgumentException("Only HTTP/HTTPS allowed");
    
    if (uri.Host == "127.0.0.1" || uri.Host == "localhost")
        throw new ArgumentException("Internal address not allowed");
    
    using (var client = new HttpClient())
    {
        return await client.GetStringAsync(url);
    }
}

csharp


9. 其他漏洞#

XSS#

Sink点Response.Write()

检查项:是否对输出进行HTML编码

LDAP注入#

Sink点DirectorySearcher.Filter

检查项:是否对LDAP过滤器参数化

表达式注入#

Sink点System.Linq.Dynamic

检查项:是否允许用户提供表达式


10. 审计工具#

  • 静态分析:CodeQL、Semgrep、SonarQube、Fortify
  • 动态检测:OWASP ZAP、Burp Suite
  • 依赖检查:NuGet Audit、Snyk