ASP.NET Core中的OWASP Top 10 十大风险-SQL注入
不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址
本博文翻译自:
https://dotnetcoretutorials.com/2017/10/11/owasp-top-10-asp-net-core-sql-injection/
OWASP或者说是开放Web应用程序安全项目,这是一个非营利性的组织,其目的是促进安全的web应用程序的开发和设计。当他们在世界各地举办不同的研讨会和活动时,你可能听说过他们,因为“OWASP Top Ten”项目。每隔几年,OWASP就会发布十大最重要的web安全风险列表。当然,这并不意味着您需要检查这10个项目,您的网站现在是安全的,但它绝对涵盖了您的网站上最常见的攻击媒介的基础。
当我第一次开始编程并听说OWASP的时候,对我来说最困难的事情就是把它变成实际的东西,例如,当有人开始谈论“CSRF”时,我想知道在.NET中,它是什么样子,它保护自己的基本原则是什么,.NET的系统中有什么(如果有的话)?这篇10部分的文章系列将尝试在ASP.net Core的领域中回答这些问题。您绝对不会突然对所有网络攻击都可以免疫,但是希望可以帮助您从ASP.net Core角度了解OWASP十大安全风险。
另外要注意的是,我将基于OWASP 2017 候选发布版的前10名。这些尚未被接受为2017年的官方十强,如果他们改变,我一定会添加额外的文章来涵盖一切。
所以不用多说,我们开始吧!我们列出的第一个项目是SQL注入。
SQL注入如何工作?
SQL注入可以通过修改一个已知的输入参数来修改SQL语句,而这个SQL语句的执行方式与我们预期的非常不同。这听起来像是一大堆的胡言乱语,我们来举个例子吧。
如果您想要在您的机器上运行整个工作代码项目,您可以从Github那里获取任何东西。您仍然可以在没有代码的情况下继续进行下去,因为我将在整个过程中发布代码示例和屏幕截图。
首先让我们创建一个包含两个表的数据库。第一个表名为“NonsensitiveDataTable”,它包含一些我们不敏感可以向用户分享的数据。第二个表名为“SensitiveDataTable”它包含用户信用卡和社会保障号码。下面是SQL Server中的表。
现在我们假设我们有一个API方法。这个方法的作用就是让我们通过id来向NonSensitiveDataTable表请求数据。
[HttpGet]
[Route("nonsensitive")]
public string GetNonSensitiveDataById()
{
using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString")))
{
connection.Open();
SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Id = {Request.Query["id"]}", connection);
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
string returnString = string.Empty;
returnString += $"Name : {reader["Name"]}. ";
returnString += $"Description : {reader["Description"]}";
return returnString;
}
else
{
return string.Empty;
}
}
}
}
这是一个相当极端的例子,编码相当差,但它说明了这一点。我们通过调用这个方法获取数据,似乎没问题。
但是只要看代码,我们就可以看到有疑问的东西。值得注意的是:
SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Id = {Request.Query["id"]}", connection);
我们正在将Id参数直接传递到SQL语句中,难道我们可以在那里输入任何东西?那么我们知道当我们要求2的ID时,“Mark Twain”的记录就会被返回。但让我们来试试这个:
哇…。那么为什么我们添加测试“OR id = 1”会突然改变一切。答案在于我们上面的代码。当我们简单地将“2”的id传递给我们的代码时,最终执行这个语句。
SELECT * FROM NonSensitiveDataTable WHERE Id = 2
但是当我们通过OR语句时,它实际上会像这样读取:
SELECT * FROM NonSensitiveDataTable WHERE Id = 2 OR Id = 1
所以在这里我们已经设法通过更改查询参数来修改执行的SQL语句。但没有什么大不了的对吗?用户将收到错误的数据,那就是结束了。那么,因为我们知道我们可以修改SQL语句。让我们尝试一些时髦的东西。让我们试试这个网址:
呃哦,看起来不好看 运行的查询结果如下所示:
SELECT * FROM NonSensitiveDataTable WHERE Id = 999 UNION SELECT * FROM SensitiveDataTable
我们在NonSensitiveDataTable表中查询ID为999的记录,并且和SensitiveDataTable表的数据关联起来,因为没有999的ID的记录我们会立即跳过,不过我们发现我们的敏感信息已经被泄露。
如果这是一个电子商务网站,他们将会有一个“客户”表和一个“订单”表等等。那么这个时候我们的信息已经被泄露,SQL注入并不像复制粘贴URL那样简单,但是有了它们我们好像有了打开王国大门的钥匙。
好的,让我们再试一次。让我们尝试以下查询。它会告诉我们数据库中的表名。
SELECT * FROM NonSensitiveDataTable WHERE Id = 999 UNION SELECT 1 as ID, Name as NAME, Name as Description FROM sys.Tables WHERE name <> 'NonSensitiveDataTable'
您经常会在web上发现SQL注入的“规则”,其中有大量的SQL命令可以尝试。你有它,少一点猜测。我们可以在这里坐上几天,把命令扔到这里。在SQL注入的情况下,对方的数据库里有什么简直一目了然
我将留给您一个最后的查询,您可以尝试并运行。假设你在试图获取数据时感到沮丧,你只是想毁掉某人的一天。想象一下运行以下命令:
SELECT * FROM NonSensitiveDataTable WHERE Id = 999; DROP TABLE SensitiveDataTable
这里我们已经放弃了,我们只需要删除一个表。也许我们甚至可以获取所有的数据,如果我们只是想让DBA的生活变得痛苦。这肯定是一种方法!
现在我们大致知道了SQL注入是什么,让我们继续保护自己!
清理您的输入
您经常会发现,关于任何描述SQL注入攻击的文章的第一条评论是“始终对您的输入进行清理”(紧接着“在您的查询中使用参数代替更好!” )- 但这些稍后再说。对您的输入进行清理可能意味着几件事情,让我们来看看。
转换为非字符串类型
在我们的示例代码中,因为我们知道我们的Id应该是一个int。我们可以这样:
int id = int.Parse(Request.Query["id"]);
SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Id = {id}", connection);
完美!现在如果有人试图在我们的查询字符串中添加进一步的信息,它不会被解析为int,我们就会避免注入!
在ASP.net Cor中一个有个更简单的例子只要允许我们的路由为我们工作,我们就可以达到上面的目的。
[HttpGet]
[Route("nonsensitiveroute/{id}")]
public string GetNonSensitiveDataByIdWithRoute(int id)
现在我们甚至不需要做任何手工解析。ASP.net Core路由将为我们处理它!
当然,如果您的查询实际上是针对整数列的,那么这能起作用。如果我们真的需要传递字符串,那又会怎样呢?
白名单/黑名单/字符替换
老实说,我不想深入到这个问题,因为我认为这是一个很容易出错的处理SQL注入的方法。但是,如果必须,您可以在传递到SQL之前,对已知正确值的白名单或黑名单运行字符串。或者您可以替换某些字符的所有实例(引号,分号等)。但是所有这些都依赖于你在攻击之前的一步。你也会把你对SQL语言的复杂性的知识与别人混淆。
虽然像PHP这样的语言具有“转义”SQL字符串的内置函数,但.NET core不支持。此外,简单地转义引号也不能从根本解决SQL注入问题.。
始终对输入进行清理需要您“记住”来做到这一点。这并不是一种可以遵循的模式。这很有效。但这不是理想的解决方案。
参数化查询
在您的SQL查询中使用参数将成为您抵御SQL注入攻击的首选。即使您正在使用某种类型的ORM(在下面讨论了一点),但在幕后,他们可能会使用参数化的查询。
那么我们如何在我们的示例项目中使用它们呢?我们将会稍微改变一下,而不是在我们的NonSensitiveDataTable表中直接使用“name”字段中进行查询。这样做只是因为它给了我们更多的灵活性。
[HttpGet]
[Route("nonsensitivewithparam")]
public string GetNonSensitiveDataByNameWithParam()
{
using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString")))
{
connection.Open();
SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Name = @name", connection);
command.Parameters.AddWithValue("@name", Request.Query["name"].ToString());
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
string returnString = string.Empty;
returnString += $"Name : {reader["Name"]}. ";
returnString += $"Description : {reader["Description"]}";
return returnString;
}
else
{
return string.Empty;
}
}
}
}
你可以看到我们在我们的查询中添加了哪些参数吗?那么这是如何工作的?那么在SQL Profiler的帮助下,正在发送的实际SQL如下所示:
exec sp_executesql N'SELECT * FROM NonSensitiveDataTable WHERE Name = @name'
,N'@name nvarchar(12)'
,@name=N'Bart Simpson'
哇...这是一个很大的不同。那么这里发生了什么?我们正在发送查询,但是说“以后,我将告诉您查询什么数据”。我们传递想要查询的确切值,而不是实际的SELECT语句。通过这种方式,我们的原始查询保持不变,并且不影响用户对查询字符串的类型。
使用参数的最好的方法是它们成为一个简单的模式。你不必“记住”来逃避某些字符串,或者“记住”使用白名单。
存储过程
SQL存储过程是避免SQL注入攻击的另一个好方法。尽管现在存储过程似乎对开发人员无所适从,但是它们与参数化查询类似,因为您将通过SELECT语句和查询数据在两个不同的“批次”中传递。我们快速创建一个存储过程来尝试(如果您使用GIT中的示例项目,则您的数据库中已经有该SP,并且不需要运行以下操作)。
CREATE PROCEDURE SP_GetNonSensitiveDataByName
@Name nvarchar(MAX)
AS
BEGIN
SET NOCOUNT ON;
SELECT * FROM NonSensitiveDataTable WHERE Name = @Name
END
让我们创建一个API方法,运行它。
[HttpGet]
[Route("nonsensitivewithsp")]
public string GetNonSensitiveDataByNameWithSP()
{
using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString")))
{
connection.Open();
SqlCommand command = new SqlCommand("SP_GetNonSensitiveDataByName", connection);
command.CommandType = System.Data.CommandType.StoredProcedure;
command.Parameters.AddWithValue("@name", Request.Query["name"].ToString());
using (var reader = command.ExecuteReader())
{
if (reader.Read())
{
string returnString = string.Empty;
returnString += $"Name : {reader["Name"]}. ";
returnString += $"Description : {reader["Description"]}";
return returnString;
}
else
{
return string.Empty;
}
}
}
}
当我们使用SQL Profiler运行这个方法时,我们可以看到以下运行。
exec SP_GetNonSensitiveDataByName @name=N'bart simpson'
如您所见,它与上面的参数化查询非常相似,我们的查询的实际细节是单独发送的。虽然现在很少看到一个围绕存储过程构建的整个项目,但它们可以保护您免受SQL注入攻击。
使用ORM
现在下一部分是减轻SQL注入攻击的有趣部分。因为在某种意义上,它成为一个一站式的保护。但是我把它放在最后的原因是因为我认为最好能理解在保护你的ORM的基础上发生了什么。
如果您使用的实体框架。当您运行Linq查询以获取数据时,任何linq“Where”语句将被打包为参数查询并发送到SQL Server。这意味着你真的需要走出自己的方式来打开自己,直到SQL注入,但这不是不可能的!几乎所有的ORM都可以发送原始的SQL查询,如果你真的想。看看Microsoft的这篇文章通过Entity Framework Core发送原始SQL。至少,使用ORM可以使SQL注入成为“默认”,而不是从外部添加一些东西。
赋予最低的权限
还记得我们的drop table命令吗?如果你忘了,下面是:
SELECT * FROM NonSensitiveDataTable WHERE Id = 999; DROP TABLE SensitiveDataTable
我们的网站真的需要删除一个表吗?不太可能然而,在我们的场景中,我们已经把王国大门的钥匙交给了外人让他去做任何它喜欢做的事。授予细粒度权限或最低权限级别是我们“停止”SQL注入攻击的最后一步。我把“停止”放在引号中,因为在现实中我们仍然容易受到敏感数据泄露的攻击,但是我们的攻击者至少不能删除表(但是它们仍然可以运行delete命令!)。
大多数SQL系统(MYSQL,Postgres,MSSQL)内置SQL角色,只允许简单的读取和写入来完成,但不能修改实际的表模式。应尽可能使用这些角色来限制任何可能的攻击。
总结
正如我们所看到的,SQL注入对于泄露敏感数据甚至是毁灭它可能是非常残酷的。但是我们也看到ASP.net Core 已经有了我们可以保护自己的方法。在依赖于输入解析/类型转换的过程中,我们可以使用参数化查询,来避免SQL注入。
欢迎转载,转载请注明翻译原文出处(本文章),原文出处(原博客地址),然后谢谢观看
如果觉得我的翻译对您有帮助,请点击推荐支持:)