User:Ilmari Karonen/NoCSRF.js

From Wikimedia Commons, the free media repository
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.
/**
 * 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 );
    }
};