XSS的防御

XSS的防御是复杂的。流行的浏览器都内置了一些对抗XSS的措施,比如Firefox的CSP,Noscript扩展,IE8内置的XSS Filter等。而对于网站来说,也应该寻找优秀的解决方案,保护用户不被XSS攻击。

 

1)HttpOnly

HttpOnly最早是由微软提出,并在IE 6中实现的,至今已成为一个标准。浏览器将禁止页面的JavaScript访问带有HttpOnly属性的Cookie。严格来说,HttpOnly并非为了对抗XSS--HttpOnly解决的是XSS后的Cookie劫持。

之前的文章中提到过”如何使用XSS窃取用户的Cookie,然后登录进该用户的账户“。但如果该Cookie设置了HttpOnly,则这种攻击会失败,因为JavaScript读取不到Cookie的值。

一个Cookie的使用过程如下:

Step1:浏览器向服务器发起请求,这时没有Cookie。

Step2:服务器返回时发送Set-Cookie头,向客户端浏览器写入Cookie。

Step3:在该Cookie到期前,浏览器访问该域下的所有页面,都将发送该Cookie。

HTTPOnly是在Set-Cookie时被标记的。服务器可能会设置多个Cookie(多个key-value对),而HttpOnly可以有选择性地加在任何一个Cookie值上。在某些时候,应用可能需要JavaScript访问某几项Cookie,这种Cookie可以不设置HttpOnly标记;而仅把HttpOnly标记给用于认证的关键Cookie。

HttpOnly的使用非常灵活,如下是一个使用HttpOnly的过程。



在这段代码中,cookie1没有HttpOnly,cookie2被标记为HttpOnly。但是只有cookie1倍JavaScript读取到:

XSS的防御_第1张图片

HttpOnly起到了应有的作用。

 

 

2)输入检查

常见的Web漏洞如XSS、SQL Injection等,都要求攻击者构造一些特殊字符,这些特殊字符可能是正常用户不会用到的,所以输入检查就有存在的必要了。

输入检查,在很多时候也被用于格式检查。例如,用户在网站注册时填写的用户名,会被要求只能为字母、数字的组合。比如 ”hello1234“ 是一个合法的用户名,而”hello#$^"就是一个非法的用户名。这些格式检查,有点像白名单,也可以让一些基于特殊字符的攻击失效。

输入检查的逻辑,必须放在服务器端代码中实现。如果只是在客户端使用JavaScript进行输入检查,是很容易被攻击者绕过的。目前Web开发的普遍做法,是同时在客户端JavaScript中和服务器代码中实现相同的输入检查。客户端JavaScript的输入检查,可以阻挡大部分误操作的正常用户,从而节约服务器资源。

在XSS的防御上,输入检查一般是检查用户输入的数据中是否包含一些特殊字符,如<、>、'、“等,如果发现存在特殊字符,者将这些特殊字符过滤或者编码。

比较只能的”输入检查“,可能还会匹配XSS的特征。比如查找用户数据中是否包含了”

其中“$var"是用户可以控制的变量。用户只要提交一个恶意脚本所在的URL地址,即可实施XSS攻击。如果是一个全局的XSS Filter,则无法看到用户的输出语境,而只能看到用户提交了一个URL,就很有可能漏报。因为大多数情况下,URL是一种合法的用户数据。

 

 

3)输出检查

一般来说,除了富文本的输出外,在变量输出到HTML页面时,可以使用编码或转义的方式来防御XSS攻击。

安全的编码函数

编码分为很多种,针对HTML代码的编码方式是HtmlEncode。HtmlEncode并非专用名词,它只是一种函数实现。  它的作用是将字符转换成HTMLEntities,对应的标准是ISO-8859-1。

为了对抗XSS,在HTMLEncode中至少转换以下字符:

XSS的防御_第2张图片

var HtmlEncode = function(str){
    var hex = new Array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f');
    var preescape = str;
    var escaped = "";
    for(var i = 0; i < preescape.length; i++){
        var p = preescape.charAt(i);
        escaped = escaped + escapeCharx(p);
    }
    
    return escaped;
                    
    function escapeCharx(original){
        var found=true;
        var thechar=original.charCodeAt(0);
        switch(thechar) {
            case 10: return "
"; break; //newline case 32: return " "; break; //space case 34:return """; break; //" case 38:return "&"; break; //& case 39:return "'"; break; //' case 47:return "/"; break; // / case 60:return "<"; break; //< case 62:return ">"; break; //> case 198:return "Æ"; break; case 193:return "Á"; break; case 194:return "Â"; break; case 192:return "À"; break; case 197:return "Å"; break; case 195:return "Ã"; break; case 196:return "Ä"; break; case 199:return "Ç"; break; case 208:return "Ð"; break; case 201:return "É"; break; case 202:return "Ê"; break; case 200:return "È"; break; case 203:return "Ë"; break; case 205:return "Í"; break; case 206:return "Î"; break; case 204:return "Ì"; break; case 207:return "Ï"; break; case 209:return "Ñ"; break; case 211:return "Ó"; break; case 212:return "Ô"; break; case 210:return "Ò"; break; case 216:return "Ø"; break; case 213:return "Õ"; break; case 214:return "Ö"; break; case 222:return "Þ"; break; case 218:return "Ú"; break; case 219:return "Û"; break; case 217:return "Ù"; break; case 220:return "Ü"; break; case 221:return "Ý"; break; case 225:return "á"; break; case 226:return "â"; break; case 230:return "æ"; break; case 224:return "à"; break; case 229:return "å"; break; case 227:return "ã"; break; case 228:return "ä"; break; case 231:return "ç"; break; case 233:return "é"; break; case 234:return "ê"; break; case 232:return "è"; break; case 240:return "ð"; break; case 235:return "ë"; break; case 237:return "í"; break; case 238:return "î"; break; case 236:return "ì"; break; case 239:return "ï"; break; case 241:return "ñ"; break; case 243:return "ó"; break; case 244:return "ô"; break; case 242:return "ò"; break; case 248:return "ø"; break; case 245:return "õ"; break; case 246:return "ö"; break; case 223:return "ß"; break; case 254:return "þ"; break; case 250:return "ú"; break; case 251:return "û"; break; case 249:return "ù"; break; case 252:return "ü"; break; case 253:return "ý"; break; case 255:return "ÿ"; break; case 162:return "¢"; break; case '\r': break; default: found=false; break; } if(!found){ if(thechar>127) { var c=thechar; var a4=c%16; c=Math.floor(c/16); var a3=c%16; c=Math.floor(c/16); var a2=c%16; c=Math.floor(c/16); var a1=c%16; return "&#x"+hex[a1]+hex[a2]+hex[a3]+hex[a4]+";"; } else{ return original; } } } }

在PHP中,有htmlentites()和htmlspecialcahrs()两个函数可以满足安全要求。

相应的,JavaScript的编码方式可以使用JavaScriptEncode。

JavaScriptEncode和HtmlEncode的编码方式不同,它需要使用”\"对特殊字符进行转义。在对抗XSS时,还要求输出的变量必须在引号内部,已避免造成安全问题。比较下面两种写法:

var x = escapeJavasript($eval);

var y = '"'+escapeJavascript($eval)+'"';

如果escapeJavaScript()函数只转义了几个危险字符,比如‘、“、<、>、\、&、#等,那么上面的两行代码输出后可能会变成:

var x = 1;alert(2);

var y = "1;alert(2)";

第一个执行了额外的代码了;第二行则是安全的。对于后者,攻击者即使想要逃逸出引号的范围,也会遇到困难:

var y = "\";alert(1);\/\/";

所以要求使用JavascriptEncode的变量输出一定要在引号内。

可是很多开发者没有这个习惯怎么办?这就只能使用一个更加严格的JavascriptEncode函数来保证安全---除了数字、字母外的所有字符,都使用十六进制 "\xHH" 的方式进行编码。

//使用“\”对特殊字符进行转义,除数字字母之外,小于127使用16进制“\xHH”的方式进行编码,大于用unicode(非常严格模式)。
var JavaScriptEncode = function(str){
     
    var hex=new Array('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f');
        
    function changeTo16Hex(charCode){
        return "\\x" + charCode.charCodeAt(0).toString(16);
    }
    
    function encodeCharx(original) {
        
        var found = true;
        var thecharchar = original.charAt(0);
        var thechar = original.charCodeAt(0);
        switch(thecharchar) {
            case '\n': return "\\n"; break; //newline
            case '\r': return "\\r"; break; //Carriage return
            case '\'': return "\\'"; break;
            case '"': return "\\\""; break;
            case '\&': return "\\&"; break;
            case '\\': return "\\\\"; break;
            case '\t': return "\\t"; break;
            case '\b': return "\\b"; break;
            case '\f': return "\\f"; break;
            case '/': return "\\x2F"; break;
            case '<': return "\\x3C"; break;
            case '>': return "\\x3E"; break;
            default:
                found=false;
                break;
        }
        if(!found){
            if(thechar > 47 && thechar < 58){ //数字
                return original;
            }
            
            if(thechar > 64 && thechar < 91){ //大写字母
                return original;
            }

            if(thechar > 96 && thechar < 123){ //小写字母
                return original;
            }        
            
            if(thechar>127) { //大于127用unicode
                var c = thechar;
                var a4 = c%16;
                c = Math.floor(c/16); 
                var a3 = c%16;
                c = Math.floor(c/16);
                var a2 = c%16;
                c = Math.floor(c/16);
                var a1 = c%16;
                return "\\u"+hex[a1]+hex[a2]+hex[a3]+hex[a4]+"";        
            }
            else {
                return changeTo16Hex(original);
            }
            
        }
    }     
  
    var preescape = str;
    var escaped = "";
    var i=0;
    for(i=0; i < preescape.length; i++){
        escaped = escaped + encodeCharx(preescape.charAt(i));
    }
    return escaped;
}

在本例中:

var x = 1;alert(2);

变成了:

var x = 1\x3balert\x282\x29

如此代码可以保证是安全的。

除了HtmlEncode、JavascriptEncode外,还有许多用于各种情况的编码函数,比如XMLEncode(与HtmlEncode类似)、JSONEncode(与JavascriptEncode类似)等。

 

 

4) 正确地防御XSS

XSS的本质还是一种“HTML注入”,用户的数据被当成了HTML代码一部分来执行,从而混淆了原本的语义,产生了新的语义。

如果网站使用了MVC框架,那么XSS就发生在View层---在应用拼接变量到HTML页面时产生。所以在用户提交数据处进行输入检查的方案,其实并不是在真正发生攻击的地方做防御。

想要根治XSS问题,可以列出所有XSS可能发生的场景,再一一解决。

下面将用变量 “$var” 表示用户数据,它将被填充入HTML代码中,可能存在以下场景。

 

1. 在HTML标签中输出

$var
$var

所有在标签中输出的变量,如果未做任何处理,都能导致直接产生XSS。

在这种场景下,XSS的利用方式一般是构造一个

或者

防御方法是变量用HtmlEncode。

 

2. 在HTML属性中输出

与在HTML标签中输出类似,可能的攻击方法:

<"" >

防御方法也是采用HtmlEncode。

在OWASP ESAPI 中推荐了一种更严格的 HtmlEcode---除了字母、数字外,其他所有的特殊字符都被编码成HTMLEntities。

String safe = ESPI.encoder().encodeForHTMLAttribute(request.getParameter("input"));

这种严格的编码方式,可以保证不会出现任何安全问题。

 

3. 在

攻击者需要先闭合引号才能实施XSS攻击:

防御时使用JavascriptEncode。

 

4. 在事件中输出

在事件中输出和在

点击标签的链接,将导致执行脚本。

XSS的防御_第3张图片

由此可见,如果用户能够完全控制URL,则可以执行脚本的方式有很多。如何解决这种情况呢?

一般来说,如果变量是整个URL,则应该先检查变量是否以”http“开头(如果不是则自动添加),以保证不会出现伪协议类的XSS攻击。

test

在此之后,再对变量进行URLEncode,即可保证不会有此类的XSS发生了。

OWASP ESAPI中有一个URLEncode的实现(此API未解决伪协议的问题):

String safe = ESAPI.encoder().encodeForURL(request.getParameter("input"));

 

 

5)处理富文本

有些时候,网站需要允许用户提交一些自定义的HTML代码,称之为”富文本“。比如一个用户在论坛里发帖,帖子的内容里要有图片、视频、表格等,这些”富文本“的效果都需要通过HTML代码来实现。

如何区分安全的”富文本“和有攻击性的XSS呢?

在处理富文本时,还是要回到”输入检查“的思路上来。”输入检查“的主要问题是,在检查时还不知道变量的输出语境。但用户提交的”富文本“数据,其语义是完整的HTML代码,在输出时也不会拼凑到某个标签的属性中。因此可以特殊情况特殊处理。

HTML是一种结构化的语言,比较好分析。通过htmlparser可以解析出HTML代码的标签、标签属性和事件。

在过滤富文本时,"事件”应该被严格禁止,因为“富文本”的展示需求里不应该包括“事件”这种动态效果。而一些危险的标签,比如