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;csharp2. 命令执行#
Sink点#
Process.Start()ProcessStartInfo.FileNameProcessStartInfo.ArgumentsProcessStartInfo.UserNameProcessStartInfo.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();
}csharp3. 文件上传和任意文件写入#
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 });
}csharp4. 反序列化漏洞#
Sink点#
BinaryFormatter.Deserialize()SoapFormatter.Deserialize()NetDataContractSerializer.ReadObject()LosFormatter.Deserialize()ObjectStateFormatter.Deserialize()JsonConvert.DeserializeObject()
审计检查项#
- 是否对不可信输入执行反序列化
- 数据来源是否可信(HTTP、Cookie)
- 是否实现了类白名单
JsonConvert的TypeNameHandling配置- 是否存在自定义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" />csharp5. 任意文件读取#
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);
}csharp6. 路径遍历#
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;
}csharp7. 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);
}csharp8. 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);
}
}csharp9. 其他漏洞#
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