1. 对输出数据进行编码
在用户将可控的数据写入页面之前,应该直接应用编码,因为要写入的上下文决定了需要使用哪种编码。例如,JavaScript字符串中的值需要不同类型的转义到HTML环境中的值。
在HTML环境中,你应该把不在白名单的值转换成HTML实体:
< converts to: <
> converts to: >
在JavaScript字符串中,非字母数字值应该被unicode转义:
< converts to: \u003c
> converts to: \u003e
有时你需要按照正确的顺序应用多层编码。例如,要将用户输入安全地嵌入到事件处理中,你需要同时处理JavaScript环境和HTML环境。所以你需要首先对输入进行unicode转义,然后对它进行html编码:
<a href="#" onclick="x='This string needs two layers of escaping'">test</a>
2. 输入校验
编码可能是XSS防御中最重要的一步,但它不足以在每个场景中防止XSS漏洞。你还应该在首次从用户接收输入时尽可能严格地校验输入。
输入校验的例子包括:
如果用户提交的URL将在响应中返回,验证它是否以安全协议(如HTTP和HTTPS)开头。否则,有人可能会利用你的网站使用有害的协议,如javascript或data。
如果用户提供了一个预期为数字的值,则验证该值实际上包含整数。
验证输入只包含预期的一组字符。
输入校验能很好的阻止非法输入。另一种方法(尝试清除非法输入以使其合法)更容易出错,应该尽可能避免。
2.1 白名单 VS 黑名单
输入校验通常应该使用白名单而不是黑名单。例如,与其列出所有有害协议(javascript、data等),不如列出安全协议(HTTP、HTTPS),并禁止任何不在列表中的协议。这将确保你的防御在新的有害协议出现时不会受到影响,并且更不容易受到攻击者试图混淆非法值以逃避黑名单的攻击。
3. 允许安全的HTML
应该尽可能避免允许用户发布HTML标记,但有时这是业务需求。例如,一个博客站点可能允许包含一些有限的HTML标记的评论发布。
传统的方法是尝试过滤掉潜在的有害标签和JavaScript。你可以尝试使用一个允许使用的安全标签和属性的白名单来实现这一点,但由于浏览器解析引擎的差异和变种XSS之类的情形,这种方法非常难以安全地实现。
还有个选项是使用在用户浏览器中执行过滤和编码的JavaScript库,例如DOMPurify。其他库允许用户提供markdown格式的内容,并将markdown转换为HTML。不幸的是,所有这些库都时不时地出现XSS漏洞,所以这不是一个完美的解决方案。如果你使用了,你应该密切监视安全更新。
除了JavaScript,其他内容,如CSS甚至常规HTML在某些情况下也可能是有害的。
4. 如何使用模板引擎防御XSS
许多现代网站使用Twig和Freemarker等服务器端模板引擎在HTML中嵌入动态内容。它们通常定义自己的转义系统。例如,在Twig中,你可以使用e()过滤器,带一个定义场景的参数:
{{ user.firstname | e('html') }}
其他一些模板引擎,如Jinja和React,默认情况下会转义动态内容,这有效地防止了大多数XSS的出现。
我们建议在评估是否使用给定的模板引擎或框架时,仔细检查转义特性。
如果你直接将用户输入连接到模板字符串中,你将容易受到服务器端模板注入的影响,这通常比XSS更严重。
5. PHP如何防御XSS
在PHP中有一个内置函数来编码实体,称为htmlentities。在HTML环境中,应该调用这个函数来转义输入。调用该函数时应该带三个参数:
你的输入字符串。
ENT_QUOTES,这是一个指定所有引号都应该被编码的标志。
字符集,在大多数情况下应该是UTF-8。
例如:
<?php echo htmlentities($input, ENT_QUOTES, 'UTF-8');?>
在JavaScript字符串环境中,你需要按照前面描述的那样对输入进行unicode转义。不幸的是,PHP没有提供用于unicode转义字符串的API。下面是一些PHP代码:
<?php
function jsEscape($str) {
$output = '';
$str = str_split($str);
for($i=0;$i<count($str);$i++) {
$chrNum = ord($str[$i]);
$chr = $str[$i];
if($chrNum === 226) {
if(isset($str[$i+1]) && ord($str[$i+1]) === 128) {
if(isset($str[$i+2]) && ord($str[$i+2]) === 168) {
$output .= '\u2028';
$i += 2;
continue;
}
if(isset($str[$i+2]) && ord($str[$i+2]) === 169) {
$output .= '\u2029';
$i += 2;
continue;
}
}
}
switch($chr) {
case "'":
case '"':
case "\n";
case "\r";
case "&";
case "\\";
case "<":
case ">":
$output .= sprintf("\\u%04x", $chrNum);
break;
default:
$output .= $str[$i];
break;
}
}
return $output;
}
?>
下面是在PHP中如何使用jsEscape函数:
<script>x = '<?php echo jsEscape($_GET['x'])?>';</script>
或者,你可以使用模板引擎。
6. 客户端JavaScript如何防御XSS
要在JavaScript中转义HTML环境中的用户输入,你需要自己的HTML编码器,因为JavaScript不提供用于编码HTML的API。下面是一些将字符串转换为HTML实体的JavaScript代码示例:
function htmlEncode(str){
return String(str).replace(/[^\w. ]/gi, function(c){
return '&#'+c.charCodeAt(0)+';';
});
}
然后你可以像下面这样使用这个函数:
<script>document.body.innerHTML = htmlEncode(untrustedValue)</script>
如果你的输入是在JavaScript字符串中,则需要执行Unicode转义的编码器。下面是一个示例unicode编码器:
function jsEscape(str){
return String(str).replace(/[^\w. ]/gi, function(c){
return '\\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4);
});
}
然后你可以像下面这样使用这个函数:
<script>document.write('<script>x="'+jsEscape(untrustedValue)+'";<\/script>')</script>
7. jQuery中如何防御XSS
jQuery中最常见的XSS形式是将用户输入传递给jQuery选择器。Web开发人员经常使用location.hash并将其传递给选择器,这将导致XSS,因为jQuery将呈现HTML。jQuery发现了这个问题,并修复了它们的选择器逻辑,以检查输入是否以hash开头。现在,jQuery只会在第一个字符是<的情况下呈现HTML。如果你将不受信任的数据传递给jQuery选择器,请确保使用上面的jsEscape函数正确地转义该值。
8. 使用内容安全策略(CSP)缓解XSS
内容安全策略(CSP)是防止跨站点脚本编制的最后一道防线。如果你的XSS防御失败,你可以使用CSP通过限制攻击者的行为来缓解XSS。
CSP允许你控制各种事情,比如是否可以加载外部脚本以及是否执行内联脚本。要部署CSP,你需要包含一个名为Content-Security-Policy的HTTP响应头和一个包含你的策略的值。
CSP的示例如下:
default-src 'self'; script-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none';
此策略指定图片和脚本等资源只能从与主页相同的来源加载。因此,即使攻击者能够成功注入XSS payload,他们也只能从当前源加载资源。这大大降低了攻击者利用XSS漏洞的机会。
如果你需要加载外部资源,请确保只允许那些不能帮助攻击者利用你的站点的脚本。例如,如果你将某些域列入白名单,那么攻击者可以从这些域加载任何脚本。在可能的情况下,尝试在你自己的域上托管资源。
如果这是不可能的,那么你可以使用hash-或基于nonce的策略来允许在不同的域上使用脚本。nonce是作为脚本或资源的属性而添加的随机字符串,只有当随机字符串与服务器生成的字符串匹配时才会执行。攻击者无法猜测随机字符串,因此无法使用有效的nonce调用脚本或资源,因此资源将不会被执行。