让我们分析一些场景:
根本不编码
<!--- our "tricky" ID --->
<cfset ID = '"><script>alert("my evil script");</script><div foo="'>
<!--- we are closing the data-href attribute, injecting our JS and start a new tag to complete the remaining tag --->
<cfoutput>
<div data-href="page.cfm?Id=#ID#"></div>
<!--- [data-href] is printed as: page.cfm?Id="><script>alert("my evil script");</script><div foo=" --->
</cfoutput>
结果
出现一个带有“我的邪恶脚本”的警告对话框。
结论
永远不要让用户输入未编码!(你已经知道了。)
将查询字符串编码为 HTML 输出
注意:您应该始终编码 HTML 属性的完整值,而不仅仅是它的一部分。
<!--- our "tricky" ID --->
<cfset ID = "&a=b?c">
<!--- we are having some reserved characters here that will confuse the browser's query string parser --->
<cfoutput>
<div data-href="#encodeForHtmlAttribute("page.cfm?Id=#ID#")#"></div>
<!--- [data-href] is printed as: page.cfm?Id=&a=b?c --->
<script>
var attr = document.getElementsByTagName('div')[0].getAttribute('data-href');
console.log(attr); <!--- page.cfm?Id=&a=b?c --->
<cfif structIsEmpty(URL)> <!--- test related: to prevent infinite redirection --->
location.href = attr;
</cfif>
</script>
</cfoutput>
<cfdump var="#URL#">
结果
请求时page.cfm
,我们将被重定向到属性page.cfm?Id=&a=b?c
的普通值。data-href
但是,范围转储URL
将向我们展示键值对:
Id: [empty string]
a: b?c
这是意料之中的,因为浏览器的查询字符串解析器无法区分字符的字面意义和技术用途。我最近在这里回答了这个问题。
结论
当有多个上下文(这里:HTML & URL/QueryString)时,对输出进行编码是不够的。
将查询字符串编码为 URL
<!--- our "tricky" ID --->
<cfset ID = 'a&b="><script>alert("my evil script");</script><div foo="'>
<!--- we are mixing in both contexts now --->
<cfoutput>
<div data-href="page.cfm?Id=#encodeForUrl(ID)#"></div>
<!--- [data-href] is printed as: page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22 --->
<script>
var attr = document.getElementsByTagName('div')[0].getAttribute('data-href');
console.log(attr); <!--- page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22 --->
<cfif structIsEmpty(URL)> <!--- test related: to prevent infinite redirection --->
location.href = attr;
</cfif>
</script>
</cfoutput>
<cfdump var="#URL#">
结果
请求时page.cfm
,我们将被重定向到属性page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22
的普通值。data-href
的范围转储URL
将向我们展示键值对:
Id: a&b="><script>alert("my evil script");</script><div foo="
这一次,浏览器的查询字符串解析器可以区分字符的字面意义和技术用途。但是这里的 HTML 上下文呢?好吧,由 完成的百分比编码encodeForUrl()
不会与 HTML 的保留字符冲突,因为%
在 HTML 中没有技术目的,也不会破坏任何东西。
结论
理论上我们到这里就完成了。无需对 URL 编码的值进行 HTML 编码,因为两种编码没有重叠。
将查询字符串编码为 URL - AND - 以输出为 HTML
<!--- our "tricky" ID --->
<cfset ID = 'a&b="><script>alert("my evil script");</script><div foo="'>
<!--- we are mixing in both contexts again --->
<cfoutput>
<div data-href="#encodeForHtmlAttribute("page.cfm?Id=#encodeForUrl(ID)#")#"></div>
<!--- [data-href] is printed as: page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22 --->
<script>
var attr = document.getElementsByTagName('div')[0].getAttribute('data-href');
console.log(attr); <!--- page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22 --->
<cfif structIsEmpty(URL)> <!--- test related: to prevent infinite redirection --->
location.href = attr;
</cfif>
</script>
</cfoutput>
<cfdump var="#URL#">
结果
请求时page.cfm
,我们将被重定向到属性page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22
的普通值。data-href
的范围转储URL
将向我们展示键值对:
Id: a&b="><script>alert("my evil script");</script><div foo="
似乎什么都没有改变,对吧?不完全是。这就是我们data-href
现在在最终 HTML 输出中的样子:
page.cfm?Id=a%26b%3D%22%3E%3Cscript%3Ealert%28%22my+evil+script%22%29%3B%3C%2Fscript%3E%3Cdiv+foo%3D%22
如您所见,percent-encoding 现在针对 HTML 进行了额外编码(%
已编码为其十六进制表示形式%
)。
结论
该值现在对于两种上下文都是安全的。
有更多的编码可以混合(想想encodeForJavaScript()
),但你明白了。它总是关于值中的哪些字符需要编码,以免因其技术目的而被误解。这最终可能会像拥有 3 到 4 个嵌套编码一样疯狂。但话又说回来:通常这些编码不会相互冲突,因此不一定需要针对所有上下文对它们进行编码。