blob: 2a9ac5ce869df3ec175b736cf4ff963399217378 [file] [log] [blame]
/**
* The OWASP CSRFGuard Project, BSD License
* Eric Sheridan (eric@infraredsecurity.com), Copyright (c) 2011
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of OWASP nor the names of its contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function() {
/**
* Code to ensure our event always gets triggered when the DOM is updated.
* @param obj
* @param type
* @param fn
* @source http://www.dustindiaz.com/rock-solid-addevent/
*/
function addEvent( obj, type, fn ) {
if (obj.addEventListener) {
obj.addEventListener( type, fn, false );
EventCache.add(obj, type, fn);
}
else if (obj.attachEvent) {
obj["e"+type+fn] = fn;
obj[type+fn] = function() { obj["e"+type+fn]( window.event ); }
obj.attachEvent( "on"+type, obj[type+fn] );
EventCache.add(obj, type, fn);
}
else {
obj["on"+type] = obj["e"+type+fn];
}
}
var EventCache = function(){
var listEvents = [];
return {
listEvents : listEvents,
add : function(node, sEventName, fHandler){
listEvents.push(arguments);
},
flush : function(){
var i, item;
for(i = listEvents.length - 1; i >= 0; i = i - 1){
item = listEvents[i];
if(item[0].removeEventListener){
item[0].removeEventListener(item[1], item[2], item[3]);
};
if(item[1].substring(0, 2) != "on"){
item[1] = "on" + item[1];
};
if(item[0].detachEvent){
item[0].detachEvent(item[1], item[2]);
};
};
}
};
}();
/** string utility functions **/
String.prototype.startsWith = function(prefix) {
return this.indexOf(prefix) === 0;
};
String.prototype.endsWith = function(suffix) {
return this.match(suffix+"$") == suffix;
};
/** hook using standards based prototype **/
function hijackStandard() {
XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
this.url = url;
this._open.apply(this, arguments);
};
XMLHttpRequest.prototype._send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(data) {
if(this.onsend != null) {
this.onsend.apply(this, arguments);
}
this._send.apply(this, arguments);
};
}
/** ie does not properly support prototype - wrap completely **/
function hijackExplorer() {
var _XMLHttpRequest = window.XMLHttpRequest;
function alloc_XMLHttpRequest() {
this.base = _XMLHttpRequest ? new _XMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
}
function init_XMLHttpRequest() {
return new alloc_XMLHttpRequest;
}
init_XMLHttpRequest.prototype = alloc_XMLHttpRequest.prototype;
/** constants **/
init_XMLHttpRequest.UNSENT = 0;
init_XMLHttpRequest.OPENED = 1;
init_XMLHttpRequest.HEADERS_RECEIVED = 2;
init_XMLHttpRequest.LOADING = 3;
init_XMLHttpRequest.DONE = 4;
/** properties **/
init_XMLHttpRequest.prototype.status = 0;
init_XMLHttpRequest.prototype.statusText = "";
init_XMLHttpRequest.prototype.readyState = init_XMLHttpRequest.UNSENT;
init_XMLHttpRequest.prototype.responseText = "";
init_XMLHttpRequest.prototype.responseXML = null;
init_XMLHttpRequest.prototype.onsend = null;
init_XMLHttpRequest.url = null;
init_XMLHttpRequest.onreadystatechange = null;
/** methods **/
init_XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
var self = this;
this.url = url;
this.base.onreadystatechange = function() {
try { self.status = self.base.status; } catch (e) { }
try { self.statusText = self.base.statusText; } catch (e) { }
try { self.readyState = self.base.readyState; } catch (e) { }
try { self.responseText = self.base.responseText; } catch(e) { }
try { self.responseXML = self.base.responseXML; } catch(e) { }
if(self.onreadystatechange != null) {
self.onreadystatechange.apply(this, arguments);
}
}
this.base.open(method, url, async, user, pass);
};
init_XMLHttpRequest.prototype.send = function(data) {
if(this.onsend != null) {
this.onsend.apply(this, arguments);
}
this.base.send(data);
};
init_XMLHttpRequest.prototype.abort = function() {
this.base.abort();
};
init_XMLHttpRequest.prototype.getAllResponseHeaders = function() {
return this.base.getAllResponseHeaders();
};
init_XMLHttpRequest.prototype.getResponseHeader = function(name) {
return this.base.getResponseHeader(name);
};
init_XMLHttpRequest.prototype.setRequestHeader = function(name, value) {
return this.base.setRequestHeader(name, value);
};
/** hook **/
window.XMLHttpRequest = init_XMLHttpRequest;
}
/** check if valid domain based on domainStrict **/
function isValidDomain(current, target) {
var result = false;
/** check exact or subdomain match **/
if(current == target) {
result = true;
} else if(%DOMAIN_STRICT% == false) {
if(target.charAt(0) == '.') {
result = current.endsWith(target);
} else {
result = current.endsWith('.' + target);
}
}
return result;
}
/** determine if uri/url points to valid domain **/
function isValidUrl(src) {
var result = false;
/** parse out domain to make sure it points to our own **/
if(src.substring(0, 7) == "http://" || src.substring(0, 8) == "https://") {
var token = "://";
var index = src.indexOf(token);
var part = src.substring(index + token.length);
var domain = "";
/** parse up to end, first slash, or anchor **/
for(var i=0; i<part.length; i++) {
var character = part.charAt(i);
if(character == '/' || character == ':' || character == '#') {
break;
} else {
domain += character;
}
}
result = isValidDomain(document.domain, domain);
/** explicitly skip anchors **/
} else if(src.charAt(0) == '#') {
result = false;
/** ensure it is a local resource without a protocol **/
} else if(!src.startsWith("//") && (src.charAt(0) == '/' || src.indexOf(':') == -1)) {
result = true;
}
return result;
}
/** parse uri from url **/
function parseUri(url) {
var uri = "";
var token = "://";
var index = url.indexOf(token);
var part = "";
/**
* ensure to skip protocol and prepend context path for non-qualified
* resources (ex: "protect.html" vs
* "/Owasp.CsrfGuard.Test/protect.html").
*/
if(index > 0) {
part = url.substring(index + token.length);
} else if(url.charAt(0) != '/') {
part = "%CONTEXT_PATH%/" + url;
} else {
part = url;
}
/** parse up to end or query string **/
var uriContext = (index == -1);
for(var i=0; i<part.length; i++) {
var character = part.charAt(i);
if(character == '/') {
uriContext = true;
} else if(uriContext == true && (character == '?' || character == '#')) {
uriContext = false;
break;
}
if(uriContext == true) {
uri += character;
}
}
return uri;
}
/** inject tokens as hidden fields into forms **/
function injectTokenForm(form, tokenName, tokenValue, pageTokens,injectGetForms) {
if (!injectGetForms) {
var method = form.getAttribute("method");
if ((typeof method != 'undefined') && method != null && method.toLowerCase() == "get") {
return;
}
}
var value = tokenValue;
var action = form.getAttribute("action");
if(action != null && isValidUrl(action)) {
var uri = parseUri(action);
value = pageTokens[uri] != null ? pageTokens[uri] : tokenValue;
}
var hidden = document.createElement("input");
hidden.setAttribute("type", "hidden");
hidden.setAttribute("name", tokenName);
hidden.setAttribute("value", value);
form.appendChild(hidden);
}
/** inject tokens as query string parameters into url **/
function injectTokenAttribute(element, attr, tokenName, tokenValue, pageTokens) {
var location = element.getAttribute(attr);
if(location != null && isValidUrl(location)) {
var uri = parseUri(location);
var value = (pageTokens[uri] != null ? pageTokens[uri] : tokenValue);
if(location.indexOf('?') != -1) {
location = location + '&' + tokenName + '=' + value;
} else {
location = location + '?' + tokenName + '=' + value;
}
try {
element.setAttribute(attr, location);
} catch (e) {
// attempted to set/update unsupported attribute
}
}
}
/** inject csrf prevention tokens throughout dom **/
function injectTokens(tokenName, tokenValue) {
/** obtain reference to page tokens if enabled **/
var pageTokens = {};
if(%TOKENS_PER_PAGE% == true) {
pageTokens = requestPageTokens();
}
/** iterate over all elements and injection token **/
var all = document.all ? document.all : document.getElementsByTagName('*');
var len = all.length;
//these are read from the csrf guard config file(s)
var injectForms = %INJECT_FORMS%;
var injectGetForms = %INJECT_GET_FORMS%;
var injectFormAttributes = %INJECT_FORM_ATTRIBUTES%;
var injectAttributes = %INJECT_ATTRIBUTES%;
for(var i=0; i<len; i++) {
var element = all[i];
/** inject into form **/
if(element.tagName.toLowerCase() == "form") {
if(injectForms) {
injectTokenForm(element, tokenName, tokenValue, pageTokens,injectGetForms);
}
if (injectFormAttributes) {
injectTokenAttribute(element, "action", tokenName, tokenValue, pageTokens);
}
/** inject into attribute **/
} else if(injectAttributes) {
injectTokenAttribute(element, "src", tokenName, tokenValue, pageTokens);
injectTokenAttribute(element, "href", tokenName, tokenValue, pageTokens);
}
}
}
/** obtain array of page specific tokens **/
function requestPageTokens() {
var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
var pageTokens = {};
xhr.open("POST", "%SERVLET_PATH%", false);
xhr.send(null);
var text = xhr.responseText;
var name = "";
var value = "";
var nameContext = true;
for(var i=0; i<text.length; i++) {
var character = text.charAt(i);
if(character == ':') {
nameContext = false;
} else if(character != ',') {
if(nameContext == true) {
name += character;
} else {
value += character;
}
}
if(character == ',' || (i + 1) >= text.length) {
pageTokens[name] = value;
name = "";
value = "";
nameContext = true;
}
}
return pageTokens;
}
/**
* Only inject the tokens if the JavaScript was referenced from HTML that
* was served by us. Otherwise, the code was referenced from malicious HTML
* which may be trying to steal tokens using JavaScript hijacking techniques.
* The token is now removed and fetched using another POST request to solve,
* the token hijacking problem.
*/
if(isValidDomain(document.domain, "%DOMAIN_ORIGIN%")) {
/** optionally include Ajax support **/
if(%INJECT_XHR% == true) {
if(navigator.appName == "Microsoft Internet Explorer") {
hijackExplorer();
} else {
hijackStandard();
}
var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest : new window.ActiveXObject("Microsoft.XMLHTTP");
var csrfToken = {};
xhr.open("POST", "%SERVLET_PATH%", false);
xhr.setRequestHeader("FETCH-CSRF-TOKEN", "1");
xhr.send(null);
var token_pair = xhr.responseText;
token_pair = token_pair.split(":");
var token_name = token_pair[0];
var token_value = token_pair[1];
XMLHttpRequest.prototype.onsend = function(data) {
if(isValidUrl(this.url)) {
this.setRequestHeader("X-Requested-With", "XMLHttpRequest")
this.setRequestHeader(token_name, token_value);
}
};
}
/** update nodes in DOM after load **/
addEvent(window,'unload',EventCache.flush);
addEvent(window,'DOMContentLoaded', function() {
injectTokens(token_name, token_value);
});
} else {
alert("OWASP CSRFGuard JavaScript was included from within an unauthorized domain!");
}
})();