最近接手之前同事的几个项目,公司利用扫描工具进行全项目扫描,发现了部分项目代码存在安全漏洞,所以需要进行项目代码修复以避免有人恶意攻击。这个任务自然而然的就落到我手上。在这里记录一下操作的过程。
扫描出来的漏洞主要有两种,一种是SQL注入,一种是XSS攻击。以下就是我的一个解决过程。
SQL注入
什么是SQL注入,百度百科这样定义的:SQL注入是将Web页面的原URL、表单域或数据包输入的参数,修改拼接成SQL语句,传递给Web服务器,进而传给数据库服务器以执行数据库命令。如Web应用程序的开发人员对用户所输入的数据或cookie等内容不进行过滤或验证(即存在注入点)就直接传输给数据库,就可能导致拼接的SQL被执行,获取对数据库的信息以及提权,发生SQL注入攻击。可能有的人还不太了解,这里做一个简单的例子以帮助理解。
// SQL注入String param = "'kmli' or 1=1";String sql = "select student from stu where name = " + param; // 拼接SQL参数PreparedStatement preparedStatement = connection.prepareStatement(sql);ResultSet resultSet = preparedStatement.executeQuery();System.out.println(resultSet.next());
输出结果为true,DB中执行的SQL为:
// 永真条件1=1成为了查询条件的一部分,可以返回所有数据,造成了SQL注入问题select student from stu where name = 'kmli' or 1=1
以上就是一个简单的SQL注入的例子。攻击者只要在查询的字段上面添加相关的恶意执行脚本就可以做到SQL注入,从而获取相关的信息。查看我们项目的SQL语句,发现也是采用类似的方式进行查询:
sql.append(" and a.ResourceId=").append(resourceId);sql.append(" and a.PeopleProperty like '").append("%").append(resourceName).append("%' ");
所以需要对sql语句进行改进,不可以使用这种直接的方式进行获取。PreparedStatement类为我们提供了解决方案,我们可以使用以下方式防止SQL注入:
// 防止SQL注入String param = "'kmli' or 1=1";String sql = "select student from stu where name = " + param;PreparedStatement preparedStatement = connection.prepareStatement(sql);preparedStatement.setString(1, param);ResultSet resultSet = preparedStatement.executeQuery();System.out.println(resultSet.next());
此时的输出结果为false,DB中执行的SQL为:
// 永真条件1=1成为了查询条件的一部分,可以返回所有数据,造成了SQL注入问题select student from stu where name = '\'kmli\' or 1=1'
我们可以看到输出的SQL文是把整个参数用引号包起来,并把参数中的引号作为转义字符,从而避免了参数也作为条件的一部分。
PreparedStatement就是为了提高statement(包括SQL,存储过程等)执行的效率,那么PreparedStatement为什么可以防止SQL注入呢?打开java.sql.PreparedStatement通用接口,看到如下注释:
An object that represents a precompiled SQL statement.A SQL statement is precompiled and stored in a PreparedStatement object. This object can then be used to efficiently execute this statement multiple times.
我们知道,SQL执行过程分为以下几个步骤:
1.Convert given SQL query into DB format -- 将SQL语句转化为DB形式(语法树结构)2.Check for syntax -- 检查语法3.Check for semantics -- 检查语义4.Prepare execution plan -- 准备执行计划5.Set the run-time values into the query -- 设置运行时的参数6.Run the query and fetch the output -- 执行查询并取得结果
在上面的解释中看到A SQL statement is precompiled,那么什么是“precompiled SQL statement”呢?“precompiled SQL statement”,就是同样的SQL文(包括不同参数的),1-4步骤只在第一次执行,所以大大提高了执行效率(特别是对于需要重复执行同一SQL的)。接下来在具体看看PreparedStatement接口的setString方法(其它设置参数的方法诸如setInt,setDouble之类,编译器会检查参数类型,已经避免了SQL注入),以下为部分代码:
public void setString(int parameterIndex, String x) throws SQLException {synchronized (checkClosed().getConnectionMutex()) {// if the passed string is null, then set this column to nullif (x == null) {setNull(parameterIndex, Types.CHAR);} else {checkClosed();int stringLength = x.length();if (this.connection.isNoBackslashEscapesSet()) {// Scan for any nasty chars// 判断是否需要转义处理(比如包含引号,换行等字符)boolean needsHexEscape = isEscapeNeededForString(x, stringLength); // 如果不需要转义,则在两边加上单引号if (!needsHexEscape) {byte[] parameterAsBytes = null;StringBuilder quotedString = new StringBuilder(x.length() + 2);quotedString.append('\'');quotedString.append(x);quotedString.append('\'');...} else {...}String parameterAsString = x;boolean needsQuoted = true;// 如果需要转义,则做转义处理if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {...
从以上注释的部分明白为什么参数会被单引号包裹,并且类似单引号之类的特殊字符会被转义处理,而就是因为这些才避免了SQL注入。 所以最后我也将代码中使用SQL的备份进行了修改:
sql.append(" and a.ResourceId = ?");parameters.set(index++,resourceId);sql.append(" and a.PeopleProperty like concat('%',?,'%') ");parameters.set(index++, resourceName);
大家看不懂上面的代码不要紧,最重要的实现原理是一样的,我这样写只是因为采用了公式的赋值方式,至此,SQL注入的问题得到了解决。
XSS攻击
XSS,跨站脚本攻击,人们经常将跨站脚本攻击(Cross Site Scripting)缩写为CSS,但这会与层叠样式表(Cascading Style Sheets,CSS)的缩写混淆。因此,有人将跨站脚本攻击缩写为XSS。百度百科这样解释的:XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上也可以包括Java、 VBScript、ActiveX、 Flash 或者甚至是普通的HTML。攻击成功后,攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。听起来和SQL有类似的地方。这里不做进一步的展开,大家不太明白的可以自行百度。
常见的XSS的攻击方式有以下几种:
1、盗用cookie,获取敏感信息。2、利用植入Flash,通过crossdomain权限设置进一步获取更高权限;或者利用Java等得到类似的操作。3、利用iframe、frame、XMLHttpRequest或上述Flash等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。4、利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。5、在访问量极大的一些页面上的XSS可以攻击一些小型网站,实现DDoS攻击的效果。
要想对XSS攻击进行防御,注意可以考虑以下几点:
(1)基于特征的防御
XSS漏洞和著名的SQL注入漏洞一样,都是利用了Web页面的编写不完善,所以每一个漏洞所利用和针对的弱点都不尽相同。这就给XSS漏洞防御带来了困难,不可能以单一特征来概括所有XSS攻击。传统的XSS防御在进行攻击鉴别时多采用特征匹配方式,主要是针对“javascript”这个关键字进行检索,但是这种鉴别不够灵活,凡是提交的信息中各有“javascript”时,就被硬性的被判定为XSS攻击。
(2)基于代码修改的防御
Web页面开发者在编写程序时往往会出现一些失误和漏洞,XSS攻击正是利用了失误和漏洞,因此一种比较理想的方法就是通过优化Web应用开发来减少漏洞,避免被攻击:1)用户向服务器上提交的信息要对URL和附带的的HTTP头、POST数据等进行查询,对不是规定格式、长度的内容进行过滤。2)实现Session标记(session tokens)、CAPTCHA系统或者HTTP引用头检查,以防功能被第三方网站所执行。3)确认接收的的内容被妥善的规范化,仅包含最小的、安全的Tag(没有javascript),去掉任何对远程内容的引用(尤其是样式表和javascript),使用HTTP only的cookie。
(3)客户端分层防御策略
客户端跨站脚本攻击的分层防御策略是基于独立分配线程和分层防御策略的安全模型。它建立在客户端(浏览器),这是它与其他模型最大的区别,之所以客户端安全性如此重要,客户端在接受服务器信息,选择性的执行相关内容。这样就可以使防御XSS攻击变得容易,该模型主要由三大部分组成:1)对每一个网页分配独立线程且分析资源消耗的“网页线程分析模块”;2)包含分层防御策略四个规则的用户输入分析模块;3)保存互联网上有关XSS恶意网站信息的XSS信息数据库。
XSS攻击主要是由程序漏洞造成的,要完全防止XSS安全漏洞主要依靠程序员较高的编程能力和安全意识,当然安全的软件开发流程及其他一些编程安全原则也可以大大减少XSS安全漏洞的发生。这些防范XSS漏洞原则包括:
(1)不信任用户提交的任何内容,对所有用户提交内容进行可靠的输入验证,包括对URL、查询关键字、HTTP头、REFER、POST数据等,仅接受指定长度范围内、采用适当格式、采用所预期的字符的内容提交,对其他的一律过滤。尽量采用POST而非GET提交表单;对“<”,“>”,“;”,“””等字符做过滤;任何内容输出到页面之前都必须加以en-code,避免不小心把htmltag显示出来。(2)实现Session 标记(session tokens)、CAPTCHA(验证码)系统或者HTTP引用头检查,以防功能被第三方网站所执行,对于用户提交信息的中的img等link,检查是否有重定向回本站、不是真的图片等可疑操作。(3)cookie 防盗。避免直接在cookie中泄露用户隐私,例如email、密码,等等;通过使cookie和系统IP绑定来降低cookie泄露后的危险。这样攻击者得到的cookie没有实际价值,很难拿来直接进行重放攻击。(4)确认接收的内容被妥善地规范化,仅包含最小的、安全的Tag(没有JavaScript),去掉任何对远程内容的引用(尤其是样式表和JavaScript),使用HTTPonly的cookie。
基于以上几点,由于项目比较老,采用的是SSM架构,所以第一步,需要在web.xml文件中加入过滤器(大家依据自己的需要改成自己过滤器的名称):
<!-- xss过滤器 --><filter><filter-name>XssFilter</filter-name><filter-class>ttd.mon.XssFilter</filter-class></filter><filter-mapping><filter-name>XssFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
第二步,在相应的包下创建对应的过滤器类:
/*** @Author likangmin* @create /11/23 13:39*/public class XssFilter implements Filter {FilterConfig filterConfig = null;public void init(FilterConfig filterConfig) throws ServletException {this.filterConfig = filterConfig;}public void destroy() {this.filterConfig = null;}public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException {chain.doFilter(new XssHttpServletRequestWrapperFilter((HttpServletRequest) request), response);}}
然后写自定义过滤器:
/*** @Author likangmin* @create /11/23 13:41*/public class XssHttpServletRequestWrapperFilter extends HttpServletRequestWrapper {HttpServletRequest orgRequest = null;public XssHttpServletRequestWrapperFilter(HttpServletRequest request) {super(request);orgRequest = request;}/*** 覆盖getParameter方法,将参数名和参数值都做xss过滤。* 如果需要获得原始的值,则通过super.getParameterValues(name)来获取* getParameterNames,getParameterValues和getParameterMap也可能需要覆盖*/@Overridepublic String getParameter(String name) {String value = super.getParameter(xssEncode(name));if (value != null) {value = xssEncode(value);value = HTMLEncode(value);}return value;}/*** spring是使用的getParameterValues方法*/@Overridepublic String[] getParameterValues(String name) {String[] value = super.getParameterValues(name);if (value == null) {return null;}for (int i = 0;i <value.length;i++) {value[i] = xssEncode(value[i]);value[i] = HTMLEncode(value[i]);}return value;}/*** 对一些特殊字符进行转义***/public static String HTMLEncode(String aText) {final StringBuilder result = new StringBuilder();final StringCharacterIterator iterator = new StringCharacterIterator(aText);char character = iterator.current();while (character != CharacterIterator.DONE) {if (character == '<') {result.append("<");} else if (character == '>') {result.append(">");} else if (character == '\"') {result.append("`");} else {result.append(character);}character = iterator.next();}return result.toString();}/*** 覆盖getHeader方法,将参数名和参数值都做xss过滤。 如果需要获得原始的值,则通过super.getHeaders(name)来获取* getHeaderNames 也可能需要覆盖*/@Overridepublic String getHeader(String name) {String value = super.getHeader(xssEncode(name));if (value != null) {value = xssEncode(value);}return value;}/*** 将容易引起xss漏洞的半角字符直接替换成全角字符* 目前xssProject对注入代码要求是必须开始标签和结束标签(如<script></script>)正确匹配才能解析,否则报错;因此只能替换调xssProject换为自定义实现** @param s* @return*/private static String xssEncode(String s) {if (s == null || s.isEmpty()) {return s;}String result = stripXSS(s);if (null != result) {result = escape(result);}return result;}public static String escape(String s) {StringBuilder sb = new StringBuilder(s.length() + 16);for (int i = 0; i < s.length(); i++) {char c = s.charAt(i);switch (c) {case '>':sb.append('>');// 全角大于号break;case '<':sb.append('<');// 全角小于号break;case '\'':sb.append('‘');// 全角单引号break;case '\"':sb.append('“');// 全角双引号break;case '\\':sb.append('\');// 全角斜线break;case '%':sb.append('%'); // 全角冒号break;default:sb.append(c);break;}}return sb.toString();}private static String stripXSS(String value) {if (value != null) {// Avoid null charactersvalue = value.replaceAll("", "");// Avoid anything between script tagsPattern scriptPattern = pile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);value = scriptPattern.matcher(value).replaceAll("");// Avoid anything in a src='...' type of expressionscriptPattern = pile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");scriptPattern = pile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");// Remove any lonesome </script> tagscriptPattern = pile("</script>", Pattern.CASE_INSENSITIVE);value = scriptPattern.matcher(value).replaceAll("");// Remove any lonesome <script ...> tagscriptPattern = pile("<script(.*?)>",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");// Avoid eval(...) expressionsscriptPattern = pile("eval\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");// Avoid expression(...) expressionsscriptPattern = pile("expression\\((.*?)\\)",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");// Avoid javascript:... expressionsscriptPattern = pile("javascript:", Pattern.CASE_INSENSITIVE);value = scriptPattern.matcher(value).replaceAll("");value = scriptPattern.matcher(value).replaceAll("");// Avoid οnlοad= expressionsscriptPattern = pile("onload(.*?)=",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");scriptPattern = pile("<iframe>(.*?)</iframe>", Pattern.CASE_INSENSITIVE);value = scriptPattern.matcher(value).replaceAll("");scriptPattern = pile("</iframe>", Pattern.CASE_INSENSITIVE);value = scriptPattern.matcher(value).replaceAll("");// Remove any lonesome <script ...> tagscriptPattern = pile("<iframe(.*?)>",Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);value = scriptPattern.matcher(value).replaceAll("");}return value;}/*** 获取最原始的request** @return*/public HttpServletRequest getOrgRequest() {return orgRequest;}/*** 获取最原始的request的静态方法** @return*/public static HttpServletRequest getOrgRequest(HttpServletRequest req) {if (req instanceof XssHttpServletRequestWrapperFilter) {return ((XssHttpServletRequestWrapperFilter) req).getOrgRequest();}return req;}}
至此,XSS攻击问题得到解决。