跳到内容

基于DOM的XSS预防备忘单

简介

在查看XSS(跨站脚本)时,通常有三种公认的XSS形式

XSS预防备忘单在处理反射型和存储型XSS方面做得很好。本备忘单旨在解决基于DOM(文档对象模型)的XSS,并且是XSS预防备忘单的扩展(并假定已理解该备忘单)。

为了理解基于DOM的XSS,需要了解反射型和存储型XSS与基于DOM的XSS之间的根本区别。主要的区别在于攻击被注入应用程序的位置。

反射型和存储型XSS是服务器端注入问题,而基于DOM的XSS是客户端(浏览器)注入问题。

所有这些代码都源于服务器,这意味着确保其免受XSS攻击是应用程序所有者的责任,无论其属于何种类型的XSS缺陷。此外,XSS攻击总是会在浏览器中执行

反射型/存储型XSS与DOM XSS的区别在于攻击被添加或注入应用程序的位置。对于反射型/存储型,攻击是在服务器端处理请求时注入应用程序的,此时不可信输入被动态添加到HTML中。对于DOM XSS,攻击是在客户端运行时直接注入应用程序的。

当浏览器渲染HTML以及任何其他相关内容(如CSS或JavaScript)时,它会识别不同种类输入的各种渲染上下文,并为每个上下文遵循不同的规则。渲染上下文与HTML标签及其属性的解析相关联。

  • 渲染上下文的HTML解析器决定了数据在页面上的呈现和布局方式,可以进一步细分为HTML、HTML属性、URL和CSS的标准上下文。
  • 执行上下文的JavaScript或VBScript解析器与脚本代码的解析和执行相关联。每个解析器在执行脚本代码的方式上都有独特且独立的语义,这使得为缓解各种上下文中的漏洞创建一致的规则变得困难。这种复杂性因执行上下文内每个子上下文(HTML、HTML属性、URL和CSS)中编码值的不同含义和处理方式而加剧。

为了本文的目的,我们将HTML、HTML属性、URL和CSS上下文称为子上下文,因为每个这些上下文都可以在JavaScript执行上下文中被访问和设置。

在JavaScript代码中,主上下文是JavaScript,但通过正确的标签和上下文关闭字符,攻击者可以尝试使用等效的JavaScript DOM方法攻击其他4个上下文。

以下是一个发生在JavaScript上下文和HTML子上下文中的漏洞示例

 <script>
 var x = '<%= taintedVar %>';
 var d = document.createElement('div');
 d.innerHTML = x;
 document.body.appendChild(d);
 </script>

让我们依次查看执行上下文的各个子上下文。

规则 #1 - 在执行上下文中的HTML子上下文中插入不可信数据之前,先进行HTML转义,然后进行JavaScript转义

有几种方法和属性可用于在JavaScript中直接渲染HTML内容。这些方法构成了执行上下文中的HTML子上下文。如果这些方法提供了不可信输入,则可能导致XSS漏洞。例如

危险HTML方法的例子

属性

 element.innerHTML = "<HTML> Tags and markup";
 element.outerHTML = "<HTML> Tags and markup";

方法

 document.write("<HTML> Tags and markup");
 document.writeln("<HTML> Tags and markup");

指南

为了安全地动态更新DOM中的HTML,我们建议

  1. HTML编码,然后
  2. JavaScript编码所有不可信输入,如以下示例所示
 var ESAPI = require('node-esapi');
 element.innerHTML = "<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForHTML(untrustedData))%>";
 element.outerHTML = "<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForHTML(untrustedData))%>";
 var ESAPI = require('node-esapi');
 document.write("<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForHTML(untrustedData))%>");
 document.writeln("<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForHTML(untrustedData))%>");

规则 #2 - 在执行上下文中的HTML属性子上下文中插入不可信数据之前,先进行JavaScript转义

执行上下文中的HTML属性子上下文与标准编码规则有所不同。这是因为在HTML属性渲染上下文中进行HTML属性编码是必要的,以缓解试图跳出HTML属性或添加可能导致XSS的额外属性的攻击。

当您处于DOM执行上下文时,您只需要对不执行代码的HTML属性(除了事件处理程序、CSS和URL属性之外的属性)进行JavaScript编码。

例如,一般规则是将不可信数据(来自数据库、HTTP请求、用户、后端系统等的数据)进行HTML属性编码并放置在HTML属性中。这是在渲染上下文中输出数据时应采取的适当步骤,然而,在执行上下文中使用HTML属性编码将破坏应用程序数据的显示。

安全但有问题(BROKEN)的例子

 var ESAPI = require('node-esapi');
 var x = document.createElement("input");
 x.setAttribute("name", "company_name");
 // In the following line of code, companyName represents untrusted user input
 // The ESAPI.encoder().encodeForHTMLAttribute() is unnecessary and causes double-encoding
 x.setAttribute("value", '<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForHTMLAttribute(companyName))%>');
 var form1 = document.forms[0];
 form1.appendChild(x);

问题在于,如果 companyName 的值为 "Johnson & Johnson"。那么在输入文本字段中将显示 "Johnson &amp; Johnson"。在上述情况下应使用的适当编码仅是JavaScript编码,以防止攻击者关闭单引号并嵌入代码,或逃逸到HTML并打开新的脚本标签。

安全且功能正确(FUNCTIONALLY CORRECT)的例子

 var ESAPI = require('node-esapi');
 var x = document.createElement("input");
 x.setAttribute("name", "company_name");
 x.setAttribute("value", '<%=ESAPI.encoder().encodeForJavascript(companyName)%>');
 var form1 = document.forms[0];
 form1.appendChild(x);

需要注意的是,在设置不执行代码的HTML属性时,值是直接在HTML元素的DOM对象属性中设置的,因此无需担心注入问题。

规则 #3 - 在执行上下文中的事件处理程序和JavaScript代码子上下文中插入不可信数据时要小心

在JavaScript代码中放置动态数据尤其危险,因为JavaScript编码对于JavaScript编码数据的语义与其他编码不同。在许多情况下,JavaScript编码无法阻止执行上下文中的攻击。例如,JavaScript编码的字符串即使经过JavaScript编码也会执行。

因此,主要建议是避免在此上下文中包含不可信数据。如果必须包含,以下示例描述了一些有效和无效的方法。

var x = document.createElement("a");
x.href="#";
// In the line of code below, the encoded data on the right (the second argument to setAttribute)
// is an example of untrusted data that was properly JavaScript encoded but still executes.
x.setAttribute("onclick", "\u0061\u006c\u0065\u0072\u0074\u0028\u0032\u0032\u0029");
var y = document.createTextNode("Click To Test");
x.appendChild(y);
document.body.appendChild(x);

setAttribute(name_string,value_string) 方法很危险,因为它隐式地将 value_string 强制转换为 name_string 的DOM属性数据类型。

在上述情况下,属性名称是一个JavaScript事件处理程序,因此属性值被隐式转换为JavaScript代码并进行评估。在上述情况下,JavaScript编码无法缓解基于DOM的XSS。

其他将代码作为字符串类型参数的JavaScript方法(如 setTimeoutsetInterval、new Function 等)也将遇到类似上述问题。这与HTML标签(HTML解析器)的事件处理程序属性中的JavaScript编码形成了鲜明对比,后者JavaScript编码可以缓解XSS。

<!-- Does NOT work  -->
<a id="bb" href="#" onclick="\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0029"> Test Me</a>

替代使用 Element.setAttribute(...) 设置DOM属性的方法是直接设置属性。直接设置事件处理程序属性将允许JavaScript编码缓解基于DOM的XSS。请注意,将不可信数据直接放入命令执行上下文始终是一种危险的设计。

<a id="bb" href="#"> Test Me</a>
//The following does NOT work because the event handler is being set to a string.
//"alert(7)" is JavaScript encoded.
document.getElementById("bb").onclick = "\u0061\u006c\u0065\u0072\u0074\u0028\u0037\u0029";

//The following does NOT work because the event handler is being set to a string.
document.getElementById("bb").onmouseover = "testIt";

//The following does NOT work because of the encoded "(" and ")".
//"alert(77)" is JavaScript encoded.
document.getElementById("bb").onmouseover = \u0061\u006c\u0065\u0072\u0074\u0028\u0037\u0037\u0029;

//The following does NOT work because of the encoded ";".
//"testIt;testIt" is JavaScript encoded.
document.getElementById("bb").onmouseover = \u0074\u0065\u0073\u0074\u0049\u0074\u003b\u0074\u0065\u0073
                                            \u0074\u0049\u0074;

//The following DOES WORK because the encoded value is a valid variable name or function reference.
//"testIt" is JavaScript encoded
document.getElementById("bb").onmouseover = \u0074\u0065\u0073\u0074\u0049\u0074;

function testIt() {
   alert("I was called.");
}

在JavaScript中还有其他一些地方,JavaScript编码被接受为有效的可执行代码。

 for(var \u0062=0; \u0062 < 10; \u0062++){
     \u0064\u006f\u0063\u0075\u006d\u0065\u006e\u0074
     .\u0077\u0072\u0069\u0074\u0065\u006c\u006e
     ("\u0048\u0065\u006c\u006c\u006f\u0020\u0057\u006f\u0072\u006c\u0064");
 }
 \u0077\u0069\u006e\u0064\u006f\u0077
 .\u0065\u0076\u0061\u006c
 \u0064\u006f\u0063\u0075\u006d\u0065\u006e\u0074
 .\u0077\u0072\u0069\u0074\u0065(111111111);

 var s = "\u0065\u0076\u0061\u006c";
 var t = "\u0061\u006c\u0065\u0072\u0074\u0028\u0031\u0031\u0029";
 window[s](t);

由于JavaScript基于国际标准(ECMAScript),JavaScript编码除了支持替代字符串表示(字符串转义)外,还支持编程结构和变量中的国际字符。

然而,HTML编码的情况正好相反。HTML标签元素是明确定义的,不支持相同标签的替代表示。因此,HTML编码不能用于允许开发者拥有 <a> 标签的替代表示,例如。

HTML编码的解除武装特性

通常,HTML编码用于禁用放置在HTML和HTML属性上下文中的HTML标签。工作示例(无HTML编码)

<a href="..." >

通常编码的例子(无效 – DNW)

&#x3c;a href=... &#x3e;

HTML编码示例,以突出与JavaScript编码值的根本区别(DNW)

<&#x61; href=...>

如果HTML编码遵循与JavaScript编码相同的语义,则上面一行可能可以渲染一个链接。这种差异使得JavaScript编码在对抗XSS的斗争中成为一种不那么可行的武器。

规则 #4 - 在执行上下文中的CSS属性子上下文中插入不可信数据之前,先进行JavaScript转义

通常,从CSS上下文执行JavaScript需要将 javascript:attackCode() 传递给CSS url() 方法,或者调用CSS expression() 方法并直接执行JavaScript代码。

根据我的经验,从执行上下文(JavaScript)调用 expression() 函数已被禁用。为了缓解CSS url() 方法的攻击,请确保对传递给CSS url() 方法的数据进行URL编码。

var ESAPI = require('node-esapi');
document.body.style.backgroundImage = "url(<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForURL(companyName))%>)";

规则 #5 - 在执行上下文中的URL属性子上下文中插入不可信数据之前,先进行URL转义,然后进行JavaScript转义

在执行和渲染上下文中解析URL的逻辑似乎是相同的。因此,在执行(DOM)上下文中的URL属性编码规则变化很小。

var ESAPI = require('node-esapi');
var x = document.createElement("a");
x.setAttribute("href", '<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForURL(userRelativePath))%>');
var y = document.createTextElement("Click Me To Test");
x.appendChild(y);
document.body.appendChild(x);

如果您使用完全限定的URL,则这会破坏链接,因为协议标识符(http:javascript:)中的冒号将被URL编码,从而阻止 httpjavascript 协议的调用。

规则 #6 - 使用安全的JavaScript函数或属性填充DOM

用不可信数据填充DOM最根本的安全方法是使用安全的赋值属性 textContent

以下是安全用法的示例。

<script>
element.textContent = untrustedData;  //does not execute code
</script>

规则 #7 - 修复基于DOM的跨站脚本漏洞

修复基于DOM的跨站脚本的最佳方法是使用正确的输出方法(sink)。例如,如果您想使用用户输入写入 div tag 元素,不要使用 innerHtml,而是使用 innerTexttextContent。这将解决问题,并且是修复基于DOM的XSS漏洞的正确方法。

在诸如 eval 等危险源中使用用户控制的输入总是一个坏主意。99% 的情况下,这表明编程习惯不好或懒惰,因此只需避免这样做,而不是尝试净化输入。

最后,为了修复我们初始代码中的问题,与其尝试正确编码输出(这很麻烦且容易出错),我们只需使用 element.textContent 将其写入内容,如下所示

<b>Current URL:</b> <span id="contentholder"></span>
...
<script>
document.getElementById("contentholder").textContent = document.baseURI;
</script>

它做的是同样的事情,但这次它不再容易受到基于DOM的跨站脚本漏洞的影响。

利用JavaScript开发安全应用的指南

基于DOM的XSS极难缓解,因为它具有很大的攻击面且缺乏浏览器之间的标准化。

以下指南旨在为开发者在开发基于Web的JavaScript应用程序(Web 2.0)时提供指导,以便他们可以避免XSS。

指南 #1 - 不可信数据应仅被视为可显示文本

避免在JavaScript代码中将不可信数据视为代码或标记。

指南 #2 - 在构建模板化JavaScript时,在数据进入应用程序时,始终将不可信数据进行JavaScript编码并作为带引号的字符串进行分隔

如以下示例所示,在构建模板化JavaScript时,在数据进入应用程序时,始终将不可信数据进行JavaScript编码并作为带引号的字符串进行分隔。

var x = "<%= Encode.forJavaScript(untrustedData) %>";

指南 #3 - 使用 document.createElement("..."), element.setAttribute("...","value"), element.appendChild(...) 及类似方法构建动态界面

document.createElement("...")element.setAttribute("...","value")element.appendChild(...) 等是构建动态界面的安全方法。

请注意,element.setAttribute 仅对有限数量的属性是安全的。

危险属性包括任何作为命令执行上下文的属性,例如 onclickonblur

安全属性的例子包括:align, alink, alt, bgcolor, border, cellpadding, cellspacing, class, color, cols, colspan, coords, dir, face, height, hspace, ismap, lang, marginheight, marginwidth, multiple, nohref, noresize, noshade, nowrap, ref, rel, rev, rows, rowspan, scrolling, shape, span, summary, tabindex, title, usemap, valign, value, vlink, vspace, width

指南 #4 - 避免将不可信数据发送到HTML渲染方法

避免使用不可信数据填充以下方法。

  1. element.innerHTML = "...";
  2. element.outerHTML = "...";
  3. document.write(...);
  4. document.writeln(...);

指南 #5 - 避免将数据隐式 eval() 的众多方法

有许多方法会隐式 eval() 传递给它们的数据,这些方法必须避免。

确保传递给这些方法的任何不可信数据是

  1. 用字符串分隔符分隔
  2. 封闭在闭包中或根据用法进行N层JavaScript编码
  3. 封装在一个自定义函数中。

确保遵循上述第3步,以确保不可信数据不会发送到自定义函数中的危险方法,或通过添加额外的编码层来处理它。

利用封闭(如 Gaz 建议)

下面的示例说明了如何使用闭包来避免双重JavaScript编码。

 var ESAPI = require('node-esapi');
 setTimeout((function(param) { return function() {
          customFunction(param);
        }
 })("<%=ESAPI.encoder().encodeForJavascript(untrustedData)%>"), y);

另一种选择是使用N层编码。

N层编码

如果您的代码如下所示,您将只需要对输入数据进行双重JavaScript编码。

setTimeout("customFunction('<%=doubleJavaScriptEncodedData%>', y)");
function customFunction (firstName, lastName)
     alert("Hello" + firstName + " " + lastNam);
}

doubleJavaScriptEncodedData 的第一层JavaScript编码(在执行时)在单引号中被反转。

然后 setTimeout 的隐式 eval 反转另一层JavaScript编码,以将正确的值传递给 customFunction

你只需要双重JavaScript编码的原因是 customFunction 函数本身没有将输入传递给另一个隐式或显式调用 eval 的方法。如果 firstName 被传递给另一个隐式或显式调用 eval() 的JavaScript方法,那么上面的 <%=doubleJavaScriptEncodedData%> 就需要更改为 <%=tripleJavaScriptEncodedData%>

一个重要的实现注意事项是,如果JavaScript代码尝试在字符串比较中使用双重或三重编码数据,那么根据数据在传递给 if 比较之前经过的 evals() 次数以及值被JavaScript编码的次数,该值可能会被解释为不同的值。

如果 A 是双重JavaScript编码的,那么以下 if 检查将返回 false。

 var x = "doubleJavaScriptEncodedA";  //\u005c\u0075\u0030\u0030\u0034\u0031
 if (x == "A") {
    alert("x is A");
 } else if (x == "\u0041") {
    alert("This is what pops");
 }

这提出了一个有趣的设计点。理想情况下,应用编码并避免上述问题的正确方法是针对数据引入应用程序的输出上下文进行服务器端编码。

然后客户端侧编码(使用JavaScript编码库,例如 node-esapi)以用于传递不可信数据的各个子上下文(DOM方法)。

以下是一些使用示例

//server-side encoding
var ESAPI = require('node-esapi');
var input = "<%=ESAPI.encoder().encodeForJavascript(untrustedData)%>";
//HTML encoding is happening in JavaScript
var ESAPI = require('node-esapi');
document.writeln(ESAPI.encoder().encodeForHTML(input));

一种选择是利用JavaScript库中的ECMAScript 5不可变属性。Gaz(Gareth)提供的另一个选择是使用特定的代码结构来限制匿名闭包的可变性。

以下是一个例子

function escapeHTML(str) {
     str = str + "''";
     var out = "''";
     for(var i=0; i<str.length; i++) {
         if(str[i] === '<') {
             out += '&lt;';
         } else if(str[i] === '>') {
             out += '&gt;';
         } else if(str[i] === "'") {
             out += '&#39;';
         } else if(str[i] === '"') {
             out += '&quot;';
         } else {
             out += str[i];
         }
     }
     return out;
}

指南 #6 - 仅在表达式的右侧使用不可信数据

仅在表达式的右侧使用不可信数据,特别是那些看起来像代码且可能传递给应用程序的数据(例如 locationeval())。

window[userDataOnLeftSide] = "userDataOnRightSide";

在表达式的左侧使用不可信的用户数据允许攻击者颠覆窗口对象的内部和外部属性,而在表达式的右侧使用用户输入则不允许直接操纵。

指南 #7 - 在DOM中进行URL编码时,请注意字符集问题

在DOM中进行URL编码时,请注意字符集问题,因为JavaScript DOM中的字符集没有明确定义(Mike Samuel)。

指南 #8 - 使用 object[x] 访问器时限制对对象属性的访问

使用 object[x] 访问器时限制对对象属性的访问(Mike Samuel)。换句话说,在不可信输入和指定对象属性之间增加一层间接。

以下是使用映射类型的问题示例

var myMapType = {};
myMapType[<%=untrustedData%>] = "moreUntrustedData";

编写上述代码的开发者试图向 myMapType 对象添加额外的键值元素。然而,这可能被攻击者用于颠覆 myMapType 对象的内部和外部属性。

更好的方法是使用以下方式

if (untrustedData === 'location') {
  myMapType.location = "moreUntrustedData";
}

指南 #9 - 在ECMAScript 5 隔离或沙箱中运行JavaScript

在ECMAScript 5 隔离或沙箱中运行您的JavaScript,以使其JavaScript API更难被破坏(Gareth Heyes 和 John Stevens)。

一些JavaScript沙箱/净化器的示例

指南 #10 - 不要 eval() JSON 以将其转换为原生JavaScript对象

不要 eval() JSON 以将其转换为原生JavaScript对象。而是使用 JSON.toJSON()JSON.parse()(Chris Schmidt)。

基于DOM的XSS缓解相关的常见问题

复杂上下文

在许多情况下,上下文并非总是直截了当。

<a href="javascript:myFunction('<%=untrustedData%>', 'test');">Click Me</a>
 ...
<script>
Function myFunction (url,name) {
    window.location = url;
}
</script>

在上面的示例中,不可信数据首先出现在渲染URL上下文(a 标签的 href 属性)中,然后变为JavaScript执行上下文(javascript: 协议处理程序),后者将不可信数据传递给执行URL子上下文(myFunctionwindow.location)。

因为数据是在JavaScript代码中引入并传递给URL子上下文的,所以适当的服务器端编码将是以下形式

<a href="javascript:myFunction('<%=ESAPI.encoder().encodeForJavascript(ESAPI.encoder().encodeForURL(untrustedData)) %>', 'test');">
Click Me</a>
 ...

或者,如果您使用ECMAScript 5和不可变的JavaScript客户端编码库,您可以这样做

<!-- server side URL encoding has been removed.  Now only JavaScript encoding on server side. -->
<a href="javascript:myFunction('<%=ESAPI.encoder().encodeForJavascript(untrustedData)%>', 'test');">Click Me</a>
 ...
<script>
Function myFunction (url,name) {
    var encodedURL = ESAPI.encoder().encodeForURL(url);  //URL encoding using client-side scripts
    window.location = encodedURL;
}
</script>

编码库的不一致性

目前有许多开源编码库

  1. OWASP ESAPI
  2. OWASP Java Encoder
  3. Apache Commons Text StringEscapeUtils,替换了 Apache Commons Lang3 中的一个
  4. Jtidy
  5. 您公司的自定义实现。

有些基于黑名单工作,有些则忽略了重要的字符,如 "<" 和 ">"。

Java Encoder是一个活跃的项目,支持HTML、CSS和JavaScript编码。

ESAPI是少数基于白名单工作并编码所有非字母数字字符的库之一。使用一个了解哪些字符可以用于在各自上下文中利用漏洞的编码库非常重要。关于所需正确编码的误解比比皆是。

编码误解

许多安全培训课程和论文主张盲目使用HTML编码来解决XSS。

这从逻辑上讲似乎是审慎的建议,因为JavaScript解析器不理解HTML编码。

然而,如果您的Web应用程序返回的页面使用 text/xhtml 的内容类型或 *.xhtml 的文件扩展名,那么HTML编码可能无法缓解XSS。

例如:

<script>
&#x61;lert(1);
</script>

上述HTML编码的值仍然可执行。如果这还不足以记住,您必须记住,当您使用DOM元素的value属性检索它们时,编码会丢失。

让我们看看示例页面和脚本

<form name="myForm" ...>
  <input type="text" name="lName" value="<%=ESAPI.encoder().encodeForHTML(last_name)%>">
 ...
</form>
<script>
  var x = document.myForm.lName.value;  //when the value is retrieved the encoding is reversed
  document.writeln(x);  //any code passed into lName is now executable.
</script>

最后,JavaScript中某些通常安全的方法在特定上下文中可能变得不安全。

通常安全的方法

一个被认为是安全的属性是 innerText

一些论文或指南主张将其用作 innerHTML 的替代品,以缓解 innerHTML 中的XSS。然而,根据 innerText 应用的标签,代码可以被执行。

<script>
 var tag = document.createElement("script");
 tag.innerText = "<%=untrustedData%>";  //executes code
</script>

innerText 功能最初由Internet Explorer引入,并在被所有主要浏览器供应商采用后,于2016年正式在HTML标准中规定。

使用变体分析检测DOM XSS

易受攻击的代码

<script>
var x = location.hash.split("#")[1];
document.write(x);
</script>

Semgrep 规则识别上述DOM XSS 链接