最近做的一个项目因为安全审计需要,需要做安全改造。其中自然就包括XSS和CSRF漏洞安全整改。关于这两个网络安全漏洞的详细说明,可以参照我本篇博客最后的参考链接。当然,我这里并不是想写一篇安全方面的专题。我要讲的是在做了XSS漏洞修复之后引发的一系列事件。
超时 本地测试的时候随便点了些页面,然后debug跟了下代码未发现任何问题。上线之后用户反馈有的页面打不开,自己去线上体验发现大部分页面正常,但是存在部分客户反馈的页面打开直接超时报错。
事件紧急处理 XSS这个漏洞修复开始不是经过我处理的,上线之后由于编码规则太严格(后面我会讲我们使用的解决方案),导致前台传入的JSON字符串中的引号全被转码,造成后台解析报错。
而我成为了那个救(bei)火(guo)英(xia)雄,需要立马解决这个问题补丁升级。我看了下以前实现的代码,有考虑到通过一个XML白名单文件,配置忽略XSS编码的请求URI。最直接的办法,是直接把我这个请求的URI加入XML白名单配置,然后补丁替换白名单文件重启服务。
但是当时我在修改的时候,考虑到可能不止这一个需要过滤的白名单,如果纯粹启动时加载的XML白名单列表。到时候还有别的URI需要忽略,那我岂不是还要再发增量补丁......
于是当时我修改的时候顺便增加了一个能力,白名单可以直接在界面配置,并且每次获取白名单列表的时候动态从数据库获取(考虑到实时请求较大,我调用的系统已有接口提供的支持缓存的查询方法)。正因为有这个“后门”我直接在线上配置了这个参数,先暂且把线上问题解决。
问题探索 解决问题第一步,自然是分析线上日志。发现线上日志的确对请求的URI中的参数做了XSS编码处理。
那问题就回到我们XSS漏洞修复的实现方式:AntiSamy(见参考链接)。其中我们的XssRequestWrapper源码如下:
public class XssRequestWrapper extends HttpServletRequestWrapper { private static Logger log = LoggerFactory.getLogger(XssRequestWrapper.class); private static Policy policy = null; static { String path = XssRequestWrapper.class.getClassLoader().getResource("security/antisamy-tinymce.xml").getFile(); log.info("policy_filepath:" + path); if (path.startsWith("file")) { //以file:开头 path = path.substring(6); } try { policy = Policy.getInstance(path); } catch (PolicyException e) { e.printStackTrace(); } } public XssRequestWrapper(HttpServletRequest request) { super(request); } // 队请求参数进行安全转码 public String getParameter(String paramString) { String str = super.getParameter(paramString); if (StringUtils.isBlank(str)) { return null; } return xssClean(str, paramString); } // 队请求头进行安全转码 public String getHeader(String paramString) { String str = super.getHeader(paramString); if (StringUtils.isBlank(str)) return null; return xssClean(str, paramString); } @SuppressWarnings({"rawtypes","unchecked"}) public Map<String, String[]> getParameterMap() { Map<String, String[]> request_map = super.getParameterMap(); Iterator iterator = request_map.entrySet().iterator(); log.debug("getParameterMap size:{}", request_map.size()); while (iterator.hasNext()) { Map.Entry me = (Map.Entry) iterator.next(); String paramsKey = (String) me.getKey(); String[] values = (String[]) me.getValue(); for (int i = 0; i < values.length; i++) { values[i] = xssClean(values[i], paramsKey); } } return request_map; } public String[] getParameterValues(String paramString) { String[] arrayOfString1 = super.getParameterValues(paramString); if (arrayOfString1 == null) return null; int i = arrayOfString1.length; String[] arrayOfString2 = new String[i]; for (int j = 0; j < i; j++) arrayOfString2[j] = xssClean(arrayOfString1[j], paramString); return arrayOfString2; } public final static String KEY_FILTER_STR = "'"; public final static String SUFFIX = "value"; //需要过滤的地方 private String xssClean(String value, String paramsKey) { String keyFilterStr = KEY_FILTER_STR; String param = paramsKey.toLowerCase(); if (param.endsWith(SUFFIX)) { // 如果参数名 if (value.contains(keyFilterStr)) { value = value.replace(keyFilterStr, "‘"); } } AntiSamy antiSamy = new AntiSamy(); try { final CleanResults cr = antiSamy.scan(value, policy); // 安全的HTML输出 return cr.getCleanHTML(); } catch (ScanException e) { log.error("antiSamy scan error", e); } catch (PolicyException e) { log.error("antiSamy policy error", e); } return value; } } 可以看到了我们项目组最终采用的策略配置文件是:antisamy-tinymce.xml,这种策略只允许传送纯文本到后台(这样做真的好吗?个人觉得这个规则太过严格),并且对请求头和请求参数都做了XSS转码。请注意这里,我们相对于参考链接中源码不同的处理方式在于:我们对请求头也进行了编码处理。