SQL Server 内置了一套加密机制,用于保护各类敏感数据。在大多数情况下,这套机制对你来说是透明的:数据在存储时自动加密,使用时会自动解密。而在某些场景下,你也可以主动选择是否对数据进行加密。具体来说,SQL Server 可为以下组件提供加密保护:
· 密码
· 存储过程、视图、触发器、用户自定义函数、默认值和规则
· 服务器与用户之间传输的数据
密码加密机制
SQL Server 会自动对分配给登录名和应用角色的密码进行加密。虽然你可以直接查看主数据库中的系统表,但看到的并非密码明文——这个机制无法修改,实际上你也根本无法绕过它。
定义加密机制
有时,对对象进行加密是为了防止某些信息被不该看到的人获取。例如,一个存储过程可能包含了所有者的商业敏感信息,这些人不希望其他人看到这部分内容,即使对方能访问系统表查看对象定义也不行。正因如此,SQL Server 允许你在创建对象时指定加密。要加密一个存储过程,可以使用以下形式的 CREATE PROCEDURE 语句:
CREATE PROCEDURE procedurename [;number]
[@parameter datatype
[VARYING][ = defaultvalue][OUTPUT]]
[, …]
[WITH RECOMPILE | ENCRYPTION | RECOMPILE, ENCRYPTION]
这里我们主要关注可选的 WITH 参数。你可以指定 RECOMPILE 或 ENCRYPTION,当然也可以两者都写。ENCRYPTION 关键字的作用是让 SQL Server 不对外公开该过程的定义文本。这样一来,当 ENCRYPTION 开启时,系统存储过程 sp_helptext 无法获取到这个用户自定义过程的文本内容。如果日后你不想加密,可以用 ALTER PROCEDURE 重新创建过程,并忽略 WITH ENCRYPTION 子句。
要使用加密功能,用户和服务器必须通过 TCP/IP 网络库进行连接。你需要运行相应的网络实用工具,勾选 Force protocol encryption 选项——之前那张表里示意的做法,如果不这么做,用户与服务器之间的连接就不会被加密。
加密并非没有代价。连接建立后,还需要额外的构造步骤,用户和服务器都需要运行代码来对数据包进行加密和解密。这会带来一些开销,编解码过程也会让整个流程变慢。不过,如果你所处的网络环境不可控,那么这种做法仍然非常有价值。
加密中缺少什么?
你可能会注意到,这份列表里少了一样东西——你表中的数据。在数据被存储之前,SQL Server 并没有提供任何内置的加密工具来帮你保护表中的数据。如果你确实需要保护存储在 SQL Server 上的数据,这里有两个建议:第一,使用 GRANT 和 DENY 关键字,控制哪些用户可以读取 SQL Server 中的数据;第二,如果真的要对数据加密,不要自己从头去造轮子——直接使用经过市场验证的商用加密算法就好。
SQL 注入攻击
SQL 注入是一种比较常见的攻击方式,恶意用户可以利用它检索你的数据、修改服务器配置,甚至在你不经意间直接黑掉你的服务器。不过,SQL 注入并不是 SQL Server 本身的问题,它本质上属于应用程序代码不严谨造成的漏洞。如果你要运行这些程序,就必须清楚其中的风险。
测点定位弱点
SQL 注入的脆弱点通常出现在开发人员在构造 WHERE 子句时直接拼接用户输入的情况下。举个简单的例子,一个 ASP 程序允许用户输入一个客户 ID,然后返回该客户对应的所有联系人姓名。如果客户 ID 是通过 ASP 页面的请求字符串传回来的,那么开发人员可能会写出下面这样的代码来获取数据:
strConn = "Provider=SQLOLEDB;Data Source=(local);" & _
"Database=Northwind;Integrated Security=SSPI"
Set cnn = Server.createObject("ADODB.Connection")
cnn.Open strConn
strQuery = "select ContactName FROM Customers " & _
"where CustomerID = '" & Request.Form("CustID") & "'"
Set rstResults = cnn.Execute(strQuery)
Response.Write(rstResults.Fields("ContactName").Value)
现在你看出问题在哪儿了吧?如果用户能猜到某个客户 ID,他就可以直接通过正常的查询来获取所有对应联系人姓名。但更可怕的是,攻击者甚至不需要知道任何客户 ID。
获得额外的数据
对于一个有经验的攻击者来说,即便不知道任何客户 ID,甚至不用去猜,他们也能拿到数据。方法很简单:在程序要求输入客户 ID 的文本框里,输入下面这样一段文本:
客户 ID:
'UNION ALL SELECT ContactName FROM Customers
WHERE CustomerID <>'
提交后,实际执行的 SQL 语句就变成了:
SELECT ContactName FROM Customers
WHERE CustomerID = ''
UNION ALL SELECT ContactName FROM Customers
WHERE CustomerID <>''
通过取空客户 ID 和非空客户 ID 的并集,这条查询会返回数据库中所有的联系人姓名。实际上,利用 UNION 技术,攻击者可以获取你数据库中绝大多数信息。再比如,输入这样的客户 ID 值:
'UNION ALL SELECT FirstName '' LastName FROM
Employees WHERE LastName <>'
它就会把 SQL 语句变成:
SELECT ContactName FROM Customers
WHERE CustomerID = ''
UNION ALL SELECT FirstName '' LastName FROM
Employees WHERE LastName <>''
看,攻击者就能直接从你的数据库里拿到第一个雇员的名字了。
更多的攻击手段
如果 SQL 注入仅仅导致数据泄露,情况已经够糟糕了,但更致命的是,熟练的攻击者可以通过这个漏洞对你数据库内的所有内容下手。注意下面这个例子:
';DROP TABLE Customers;--
实际执行的 SQL 语句变成了:
SELECT ContactName FROM Customers
WHERE CustomerID = ''
; DROP TABLE Customers;-- '
这里的分号把原来的语句切断了,所以实际上执行了两条语句:第一条查了不存在的客户 ID,第二条直接删掉了整个 Customers 表。而后面加的两个杠(——)是 SQL Server 的注释符,作用是避免后续单引号引发语法错误。
攻击者可以基于这个思路的变种,运行任意的 SQL 语句或存储过程。如果再配合使用 xp_cmdshell 扩展存储过程,甚至可以直接在操作系统上执行命令——这显然是一个极其严重的漏洞。
如何保护自己的数据库
到了这一步,你知道该怎么防范 SQL 注入攻击了吧?首要的一条:绝对不能基于用户输入来拼接 WHERE 子句。你应该使用参数化查询或存储过程。回到最初的 ASP 页面,重写后的代码应该和我们刚才看到的那些示例完全不同。哪怕你觉得自己的应用程序没有漏洞,也应该遵循最小特权原则——只给用户他们真正需要的最小权限。结合我们前面讨论的其他安全技术,只允许用户访问他们应有的内容。这样,即便你还没有发现数据库中的全部脆弱点,也不至于让整个数据库瞬间崩溃。
最后的建议
以上就是 SQL Server 安全系列的全部内容。你现在未必已经成为了一名全面专家,但可以肯定的是,你了解了很多之前没接触过的东西。下一步,就是拿这些知识去保护你自己的 SQL Server 数据。记住今天学到的这些要点,应用到你自己的数据库中,确保你的数据不被黑客利用。
