Reputation: 4067
Take the following example...
<cfset ascii32to126 = "!""##$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~">
<cfoutput>
<input type="button" value="Submit" onclick="window.location='xss.cfm?demo1=#encodeForHtmlAttribute(ascii32to126)#'"><br>
window.location='xss.cfm?demo1=!"##$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~'<br>
OBVIOUSLY THIS ONE IS INCORRECT
<br><br>
<input type="button" value="Submit" onclick="window.location='xss.cfm?demo1=#encodeForUrl(ascii32to126)#'"><br>
window.location='xss.cfm?demo1=%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E%5F%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D%7E'
<br><br>
<input type="button" value="Submit" onclick="window.location='xss.cfm?demo1=#encodeForHtmlAttribute(encodeForUrl(ascii32to126))#'"><br>
window.location='xss.cfm?demo1=%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E%5F%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D%7E'
<br><br>
<input type="button" value="Submit" onclick="window.location='xss.cfm?demo1=#encodeForHtmlAttribute(encodeForJavaScript(encodeForUrl(ascii32to126)))#'"><br>
window.location='xss.cfm?demo1=\u002521\u002522\u002523\u002524\u002525\u002526\u002527\u002528\u002529\u00252A\u00252B\u00252C\u00252D\u00252E\u00252F0123456789\u00253A\u00253B\u00253C\u00253D\u00253E\u00253F\u002540ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00255B\u00255C\u00255D\u00255E\u00255F\u002560abcdefghijklmnopqrstuvwxyz\u00257B\u00257C\u00257D\u00257E'
</cfoutput>
I believe the last example is the one that is properly protected against XSS because ultimately this results in a URL (so encodeForUrl), then take a step back and you are now in the JavaScript context (so encodeForJavaScript), then take another step back and you are now in the HTML attribute context (so encodeForHtmlAttribute). I've always thought that was the process, start with how the code will ultimately be used and work your way backwards. Am I understanding this correctly? Thanks!
Also, technically the encodeForUrl() does what encodeForHtmlAttribute() does so the encodeForHtmlAttribute() could be removed from the last button and nothing would change at all but I prefer to do it because it helps in teaching and understanding.
Upvotes: 3
Views: 258
Reputation: 7833
Yes, nesting the context is the proper way to handle this. However, it's unlikely that the result on the deepest encoding will break the surrounding context, because the ESAPI functions are as strict as possible and escapes do not overlap in these contexts. So unless you need to handle untrusted input, there's no need to nest the encodings.
encodeForHtmlAttribute( encodeForJavaScript( encodeForUrl(ascii32to126) ) )
Example:
encodeForURL("&")
>> %26
encodeForJavaScript("&")
>> \x26
encodeForHTML("&")
>> &
All of the control characters here are safe for other contexts. %
is safe in JavaScript and HTML. \
is safe in URL and HTML. &#;
is safe in URL and JavaScript. That doesn't necessarily mean the output will be properly displayed, but at least it's guaranteed to be interpreted as literal string and doesn't expose any security risks.
Conclusion: Using only encodeForUrl("1&b=2&c=3")
in my example below is sufficient to be safe against XSS, because the encoded output is a literal string in JavaScript and HTML and properly decoded as URL.
NOTE: My original answer was based on an outdated server setup of ColdFusion 10. Back then the input of the
encodeFor
functions were normalized/canonicalized and thus made nesting a security risk since only the last call was actually encoding the input.
No, nested encoding will make you vulnerable (if and only if the input to each encodeFor function is normalized, see comments).
Consider the following demo:
<cfset qs = encodeForHtmlAttribute( encodeForJavaScript( encodeForUrl("1&b=2&c=3") ) )>
<cfoutput>
<button onclick="window.location='index.cfm?a=#qs#'">click me</button>
</cfoutput>
<cfdump var="#URL#">
This will result in 3 separate query parameters (a
, b
, c
), although you desired the value 1&b=2&c=3
in a single query parameter a
. The reason is: normalization. The encoder functions will normalize the input before encoding to prevent double-encoding attacks.
Now let's try encoding the actual context only once:
<cfset qs = encodeForUrl("1&b=2&c=3")>
<cfoutput>
<button onclick="window.location='index.cfm?a=#qs#'">click me</button>
</cfoutput>
<cfdump var="#URL#">
Encoding once is sufficient, because escape characters do not overlap in these contexts.
Upvotes: 3