User:Ilmari Karonen/NoCSRF.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at User:Ilmari Karonen/NoCSRF. |
/**
* NoCSRF.js
*
* This script provides a simple way for other scripts to pass parameters to
* themselves in URLs without the risk that someone else could spoof such URLs
* and thereby cause the user to perform a scripted action which they did not
* intend (an attack known as Cross-Site Request Forgery).
*
* NOTE: This is a draft version of the script. It should work, but has not
* been tested yet. Also, incompatible API changes are possible.
*/
/*
* This script relies on a JavaScript implementation of the MD5 cryptographic
* hash function. Note that, while practical collision attacks on MD5 have
* been demonstrated, this script uses MD5 in the HMAC construction to which
* the known attacks do not directly apply.
*/
importScript("MediaWiki:MD5.js");
if (typeof( NoCSRF ) == 'undefined') var NoCSRF = {
timestamp: Math.floor((new Date ()).getTime() / 1000),
key: undefined,
/**
* Sign and timestamp a URL by appending an extra "nocsrf" query parameter to
* it. Leading parts of the URL, such as the protocol, hostname or path, may
* be omitted, but if so, those parts should also be stripped from the URL when
* it is passed to NoCSRF.check_url() for verification. Additional non-URL
* data may be included in the calculation via the optional second parameter.
*
* @param url URL (or URL fragment) to sign
* @param extra_data Additional data to include in MAC calculation (optional, default = "")
* @return URL with an extra "nocsrf" query parameter appended
*/
sign_url: function (url, extra_data) {
if (typeof( extra_data ) == 'undefined') extra_data = "";
var mac = NoCSRF.mac( url + "|" + extra_data + "|" + NoCSRF.timestamp );
return url + (url.indexOf("?") < 0 ? "?" : "&") + "nocsrf=" + NoCSRF.timestamp + ":" + mac;
},
/**
* Verify that an URL has been signed by NoCSRF.sign_url() using the same key
* and extra data. Optionally, the timestamp included in the signature may
* also be checked against a maximum age limit to prevent replay attacks.
* The return value is "OK" for success or a string describing which
* verification step failed; while the returned strings _could_ be presented
* directly to the user, the intention is that the caller should check the
* string against known return values and present a less terse error message
* to the user. The currently defined return values are:
*
* "OK" : The input URL has been correctly signed and has not expired.
* "parse error" : The input does not look like the output of NoCSRF.sign_url().
* "too old" : The timestamp in the URL is more than max_age seconds old.
* "too new" : The timestamp in the URL is in the future.
* "MAC mismatch" : The signature check failed -- the URL might be forged.
*
* @param signed_url URL (or URL fragment) to verify, as returned by NoCSRF.sign_url()
* @param extra_data Additional data to include in MAC calculation (optional, default = "")
* @param max_age Maximum accepted age of URL in seconds (optional)
* @return "OK" or error string
*/
check_url: function (signed_url, extra_data, max_age) {
if (typeof( extra_data ) == 'undefined') extra_data = "";
var match = /^(.*)[?&]nocsrf=([0-9]+):([0-9a-f]{32})$/.exec(signed_url);
if (!match) return "parse error";
var url = match[1];
var ts = match[2];
var mac = match[3];
var age = NoCSRF.timestamp - ts;
if ( mac != NoCSRF.mac( url + "|" + extra_data + "|" + ts ) ) return "MAC mismatch";
if ( max_age && age > max_age ) return "too old";
if ( age < -10 ) return "too new"; // allow for 10 secs of clock weirdness
return "OK";
},
/**
* A helper function which computes the MAC of a given string using the random
* key returned by NoCSRF.get_key(). You can use this to roll your own custom
* implementations of NoCSRF.sign_url() and NoCSRF.check_url() if the standard
* ones do not fit your needs.
*
* @param string String to sign
* @return MAC of the input string using the internal key
*/
mac: function (string) {
return hex_hmac_md5( NoCSRF.get_key(), string );
},
/**
* An internal function to retrieve (and, if necessary, generate) the MAC key.
* The key is based on a pseudo-random "master key" and the user's username.
* The master key, in turn, is stored in a cookie and is generated by hashing
* a number of variables hopefully containing enough entropy to make it hard
* for an attacker to guess. (To do: Add more / better entropy sources?)
*/
get_key: function () {
if (typeof( NoCSRF.key ) != 'undefined') return NoCSRF.key;
var hash;
var match = /(^|; ?)NoCSRF-random-cookie=([0-9a-f]{32})($|;)/.exec(document.cookie);
if (match && match[2]) {
hash = match[2];
} else {
// no cookie found, generate one: we take as many entropy sources as we can and hash them
var entropy_sources = [ wgUserName, wgPageName, window.wgTrackingToken, (new Date ()).getTime(),
Math.random(), location.href, document.cookie, document.referrer ];
hash = hex_hmac_md5( "nocsrf", entropy_sources.join("|") );
document.cookie = "NoCSRF-random-cookie="+hash+"; path=/";
}
return NoCSRF.key = hex_hmac_md5( hash, wgUserName );
}
};