From 8788025abdf9a701e5b7bc88b67f7d6698c264b3 Mon Sep 17 00:00:00 2001 From: Ben Nottelling Date: Fri, 6 Aug 2021 18:58:21 -0700 Subject: [PATCH 1/4] Include a slightly modified version of CSRF-Protector-PHP --- .../CSRF-Protector-PHP/js/csrfprotector.js | 362 +++++++++++ .../Security/CSRF-Protector-PHP/js/index.php | 7 + .../libs/csrf/LoggerInterface.php | 26 + .../libs/csrf/csrfpAction.php | 49 ++ .../libs/csrf/csrfpCookieConfig.php | 70 ++ .../libs/csrf/csrfpDefaultLogger.php | 40 ++ .../libs/csrf/csrfprotector.php | 599 ++++++++++++++++++ .../CSRF-Protector-PHP/libs/csrf/index.php | 7 + .../CSRF-Protector-PHP/libs/index.php | 7 + .../Security/CSRF-Protector-PHP/licence.md | 13 + src/bb-load.php | 4 + src/csrfp-config.php | 28 + 12 files changed, 1212 insertions(+) create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/js/index.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/LoggerInterface.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfpAction.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfpCookieConfig.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfpDefaultLogger.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfprotector.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/index.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/libs/index.php create mode 100644 src/bb-library/Security/CSRF-Protector-PHP/licence.md create mode 100644 src/csrfp-config.php diff --git a/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js b/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js new file mode 100644 index 000000000..84816f0fe --- /dev/null +++ b/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js @@ -0,0 +1,362 @@ +/** + * ================================================================= + * Javascript code for OWASP CSRF Protector + * Task it does: Fetch csrftoken from cookie, and attach it to every + * POST request + * Allowed GET url + * -- XHR + * -- Static Forms + * -- URLS (GET only) + * -- dynamic forms + * ================================================================= + */ + +var CSRFP_FIELD_TOKEN_NAME = 'csrfp_hidden_data_token'; +var CSRFP_FIELD_URLS = 'csrfp_hidden_data_urls'; + +var CSRFP = { + CSRFP_TOKEN: 'CSRFP-Token', + /** + * Array of patterns of url, for which csrftoken need to be added + * In case of GET request also, provided from server + * + * @var {Array} + */ + checkForUrls: [], + /** + * Function to check if a certain url is allowed to perform the request + * With or without csrf token + * + * @param {string} url + * + * @return {Boolean} true if csrftoken is not needed + * false if csrftoken is needed + */ + _isValidGetRequest: function(url) { + for (var i = 0; i < CSRFP.checkForUrls.length; i++) { + var match = CSRFP.checkForUrls[i].exec(url); + if (match !== null && match.length > 0) { + return false; + } + } + return true; + }, + /** + * Function to get Auth key from cookie and return it to requesting function + * + * @param: void + * + * @return {string|Boolean} csrftoken retrieved from cookie + */ + _getAuthKey: function() { + var re = new RegExp(CSRFP.CSRFP_TOKEN +"=([^;]+)(;|$)"); + var RegExpArray = re.exec(document.cookie); + + if (RegExpArray === null) { + return false; + } + return RegExpArray[1]; + }, + /** + * Function to get domain of any url + * + * @param {string} url + * + * @return {string} domain of url + */ + _getDomain: function(url) { + if (url.indexOf("http://") !== 0 + && url.indexOf("https://") !== 0) + return document.domain; + return /http(s)?:\/\/([^\/]+)/.exec(url)[2]; + }, + /** + * Function to create and return a hidden input element + * For storing the CSRFP_TOKEN + * + * @param: void + * + * @return {HTMLInputElement} input element + */ + _getInputElt: function() { + var hiddenObj = document.createElement("input"); + hiddenObj.setAttribute('name', CSRFP.CSRFP_TOKEN); + hiddenObj.setAttribute('class', CSRFP.CSRFP_TOKEN); + hiddenObj.type = 'hidden'; + hiddenObj.value = CSRFP._getAuthKey(); + return hiddenObj; + }, + /** + * Returns absolute path for relative path + * + * @param {string} base base url + * @param {string} relative relative url + * + * @return {string} absolute path + */ + _getAbsolutePath: function(base, relative) { + var stack = base.split("/"); + var parts = relative.split("/"); + // remove current file name (or empty string) + // (omit if "base" is the current folder without trailing slash) + stack.pop(); + + for (var i = 0; i < parts.length; i++) { + if (parts[i] === ".") + continue; + if (parts[i] === "..") + stack.pop(); + else + stack.push(parts[i]); + } + return stack.join("/"); + }, + /** + * Remove jcsrfp-token run fun and then put them back + * + * @param {function} fun + * @param {object} obj reference form obj + * + * @return function + */ + _csrfpWrap: function(fun, obj) { + return function(event) { + // Remove CSRf token if exists + if (typeof obj[CSRFP.CSRFP_TOKEN] !== 'undefined') { + var target = obj[CSRFP.CSRFP_TOKEN]; + target.parentNode.removeChild(target); + } + + // Trigger the functions + var result = fun.apply(this, [event]); + + // Now append the CSRFP-Token back + obj.appendChild(CSRFP._getInputElt()); + + return result; + }; + }, + /** + * Initialises the CSRFProtector js script + * + * @param: void + * + * @return void + */ + _init: function() { + CSRFP.CSRFP_TOKEN = document.getElementById(CSRFP_FIELD_TOKEN_NAME).value; + try { + CSRFP.checkForUrls = JSON.parse(document.getElementById(CSRFP_FIELD_URLS).value); + } catch (err) { + console.error(err); + console.error('[ERROR] [CSRF Protector] unable to parse blacklisted url fields.'); + } + + //convert these rules received from php lib to regex objects + for (var i = 0; i < CSRFP.checkForUrls.length; i++) { + CSRFP.checkForUrls[i] = CSRFP.checkForUrls[i].replace(/\*/g, '(.*)') + .replace(/\//g, "\\/"); + CSRFP.checkForUrls[i] = new RegExp(CSRFP.checkForUrls[i]); + } + + } + +}; + +//========================================================== +// Adding tokens, wrappers on window onload +//========================================================== + +function csrfprotector_init() { + + // Call the init function + CSRFP._init(); + + // definition of basic FORM submit event handler to intercept the form request + // and attach a CSRFP TOKEN if it's not already available + var BasicSubmitInterceptor = function(event) { + if (typeof event.target[CSRFP.CSRFP_TOKEN] === 'undefined') { + event.target.appendChild(CSRFP._getInputElt()); + } else { + //modify token to latest value + event.target[CSRFP.CSRFP_TOKEN].value = CSRFP._getAuthKey(); + } + }; + + //================================================================== + // Adding csrftoken to request resulting from
submissions + // Add for each POST, while for mentioned GET request + // TODO - check for method + //================================================================== + // run time binding + document.querySelector('body').addEventListener('submit', function(event) { + if (event.target.tagName.toLowerCase() === 'form') { + BasicSubmitInterceptor(event); + } + }); + + // initial binding + // for(var i = 0; i < document.forms.length; i++) { + // document.forms[i].addEventListener("submit", BasicSubmitInterceptor); + // } + + //================================================================== + // Adding csrftoken to request resulting from direct form.submit() call + // Add for each POST, while for mentioned GET request + // TODO - check for form method + //================================================================== + HTMLFormElement.prototype.submit_ = HTMLFormElement.prototype.submit; + HTMLFormElement.prototype.submit = function() { + // check if the FORM already contains the token element + if (!this.getElementsByClassName(CSRFP.CSRFP_TOKEN).length) + this.appendChild(CSRFP._getInputElt()); + this.submit_(); + }; + + + /** + * Add wrapper for HTMLFormElements addEventListener so that any further + * addEventListens won't have trouble with CSRF token + * todo - check for method + */ + HTMLFormElement.prototype.addEventListener_ = HTMLFormElement.prototype.addEventListener; + HTMLFormElement.prototype.addEventListener = function(eventType, fun, bubble) { + if (eventType === 'submit') { + var wrapped = CSRFP._csrfpWrap(fun, this); + this.addEventListener_(eventType, wrapped, bubble); + } else { + this.addEventListener_(eventType, fun, bubble); + } + }; + + /** + * Add wrapper for IE's attachEvent + * todo - check for method + * todo - typeof is now obsolete for IE 11, use some other method. + */ + if (typeof HTMLFormElement.prototype.attachEvent !== 'undefined') { + HTMLFormElement.prototype.attachEvent_ = HTMLFormElement.prototype.attachEvent; + HTMLFormElement.prototype.attachEvent = function(eventType, fun) { + if (eventType === 'onsubmit') { + var wrapped = CSRFP._csrfpWrap(fun, this); + this.attachEvent_(eventType, wrapped); + } else { + this.attachEvent_(eventType, fun); + } + } + } + + + //================================================================== + // Wrapper for XMLHttpRequest & ActiveXObject (for IE 6 & below) + // Set X-No-CSRF to true before sending if request method is + //================================================================== + + /** + * Wrapper to XHR open method + * Add a property method to XMLHttpRequest class + * @param: all parameters to XHR open method + * @return: object returned by default, XHR open method + */ + function new_open(method, url, async, username, password) { + this.method = method; + var isAbsolute = (url.indexOf("./") === -1); + if (!isAbsolute) { + var base = location.protocol +'//' +location.host + + location.pathname; + url = CSRFP._getAbsolutePath(base, url); + } + if (method.toLowerCase() === 'get' + && !CSRFP._isValidGetRequest(url)) { + //modify the url + if (url.indexOf('?') === -1) { + url += "?" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + } else { + url += "&" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + } + } + + return this.old_open(method, url, async, username, password); + } + + /** + * Wrapper to XHR send method + * Add query parameter to XHR object + * + * @param: all parameters to XHR send method + * + * @return: object returned by default, XHR send method + */ + function new_send(data) { + if (this.method.toLowerCase() === 'post') { + // attach the token in request header + this.setRequestHeader(CSRFP.CSRFP_TOKEN, CSRFP._getAuthKey()); + } + return this.old_send(data); + } + + if (window.XMLHttpRequest) { + // Wrapping + XMLHttpRequest.prototype.old_send = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.old_open = XMLHttpRequest.prototype.open; + XMLHttpRequest.prototype.open = new_open; + XMLHttpRequest.prototype.send = new_send; + } + if (typeof ActiveXObject !== 'undefined') { + ActiveXObject.prototype.old_send = ActiveXObject.prototype.send; + ActiveXObject.prototype.old_open = ActiveXObject.prototype.open; + ActiveXObject.prototype.open = new_open; + ActiveXObject.prototype.send = new_send; + } + //================================================================== + // Rewrite existing urls ( Attach CSRF token ) + // Rules: + // Rewrite those urls which matches the regex sent by Server + // Ignore cross origin urls & internal links (one with hashtags) + // Append the token to those url already containing GET query parameter(s) + // Add the token to those which does not contain GET query parameter(s) + //================================================================== + + for (var i = 0; i < document.links.length; i++) { + document.links[i].addEventListener("mousedown", function(event) { + var href = event.target.href; + if(typeof href === "string") + { + var urlParts = href.split('#'); + var url = urlParts[0]; + var hash = urlParts[1]; + + if(CSRFP._getDomain(url).indexOf(document.domain) === -1 + || CSRFP._isValidGetRequest(url)) { + //cross origin or not to be protected by rules -- ignore + return; + } + + if (url.indexOf('?') !== -1) { + if(url.indexOf(CSRFP.CSRFP_TOKEN) === -1) { + url += "&" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + } else { + url = url.replace(new RegExp(CSRFP.CSRFP_TOKEN +"=.*?(&|$)", 'g'), + CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey() + "$1"); + } + } else { + url += "?" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + } + + event.target.href = url; + if (typeof hash !== 'undefined') { + event.target.href += '#' +hash; + } + } + }); + } + +} + +window.addEventListener("DOMContentLoaded", function() { + csrfprotector_init(); + + // Dispatch an event so clients know the library has initialized + var postCsrfProtectorInit = new Event('postCsrfProtectorInit'); + window.dispatchEvent(postCsrfProtectorInit); +}, false); diff --git a/src/bb-library/Security/CSRF-Protector-PHP/js/index.php b/src/bb-library/Security/CSRF-Protector-PHP/js/index.php new file mode 100644 index 000000000..6fab721c9 --- /dev/null +++ b/src/bb-library/Security/CSRF-Protector-PHP/js/index.php @@ -0,0 +1,7 @@ +path = $cfg['path']; + } + + if (isset($cfg['domain'])) { + $this->domain = $cfg['domain']; + } + + if (isset($cfg['secure'])) { + $this->secure = (bool) $cfg['secure']; + } + + if (isset($cfg['expire']) && $cfg['expire']) { + $this->expire = (int)$cfg['expire']; + } + } + } + } +} diff --git a/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfpDefaultLogger.php b/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfpDefaultLogger.php new file mode 100644 index 000000000..dffb667bd --- /dev/null +++ b/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfpDefaultLogger.php @@ -0,0 +1,40 @@ + + */ + private static $cookieConfig = null; + + /** + * Variable: $logger + * Logger class object + * @var LoggerInterface + */ + private static $logger = null; + + /** + * Variable: $tokenHeaderKey + * Key value in header array, which contain the token + * @var string + */ + private static $tokenHeaderKey = null; + + /* + * Variable: $requestType + * Variable to store whether request type is post or get + * @var string + */ + protected static $requestType = "GET"; + + /* + * Variable: $config + * config file for CSRFProtector + * @var int Array, length = 6 + * Property: #1: failedAuthAction (int) => action to be taken in case + * autherisation fails. + * Property: #3: customErrorMessage (string) => custom error message to + * be sent in case of failed authentication. + * Property: #4: jsFile (string) => location of the CSRFProtector js + * file. + * Property: #5: tokenLength (int) => default length of hash. + * Property: #6: disabledJavascriptMessage (string) => error message if + * client's js is disabled. + * + * TODO(mebjas): this field should be private + */ + public static $config = array(); + + /* + * Variable: $requiredConfigurations + * Contains list of those parameters that are required to be there + * in config file for csrfp to work + * + * TODO(mebjas): this field should be private + */ + public static $requiredConfigurations = array( + 'failedAuthAction', 'jsUrl', 'tokenLength'); + + /* + * Function: function to initialise the csrfProtector work flow + * + * Parameters: + * $length - (int) length of CSRF_AUTH_TOKEN to be generated. + * $action - (int array), for different actions to be taken in case of + * failed validation. + * $logger - (LoggerInterface) custom logger class object. + * + * Returns: + * void + * + * Throws: + * configFileNotFoundException - when configuration file is not found + * incompleteConfigurationException - when all required fields in config + * file are not available + */ + public static function init($length = null, $action = null, $logger = null) + { + // Check if init has already been called. + if (count(self::$config) > 0) { + throw new alreadyInitializedException("OWASP CSRFProtector: library was already initialized."); + } + + // If mod_csrfp already enabled, no extra verification needed. + if (getenv('mod_csrfp_enabled')) { + return; + } + + // Start session in case its not, and unit test is not going on + if (session_id() == '' && !defined('__CSRFP_UNIT_TEST__')) { + session_start(); + } + + // Load configuration file and properties & Check locally for a + // config.php then check for a config/csrf_config.php file in the + // root folder for composer installations + $standard_config_location = BB_PATH_ROOT ."/csrfp-config.php"; + + if (file_exists($standard_config_location)) { + self::$config = include($standard_config_location); + } else { + throw new configFileNotFoundException( + "OWASP CSRFProtector: configuration file not found for CSRFProtector!"); + } + + // Overriding length property if passed in parameters + if ($length != null) { + self::$config['tokenLength'] = intval($length); + } + + // Action that is needed to be taken in case of failed authorisation + if ($action != null) { + self::$config['failedAuthAction'] = $action; + } + + if (self::$config['CSRFP_TOKEN'] == '') { + self::$config['CSRFP_TOKEN'] = CSRFP_TOKEN; + } + + self::$tokenHeaderKey = 'HTTP_' .strtoupper(self::$config['CSRFP_TOKEN']); + self::$tokenHeaderKey = str_replace('-', '_', self::$tokenHeaderKey); + + // Load parameters for setcookie method + if (!isset(self::$config['cookieConfig'])) { + self::$config['cookieConfig'] = array(); + } + + self::$cookieConfig = new csrfpCookieConfig(self::$config['cookieConfig']); + + // Validate the config if everything is filled out + $missingConfiguration = []; + foreach (self::$requiredConfigurations as $value) { + if (!isset(self::$config[$value]) || self::$config[$value] === '') { + $missingConfiguration[] = $value; + } + } + + if ($missingConfiguration) { + throw new incompleteConfigurationException( + 'OWASP CSRFProtector: Incomplete configuration file: missing ' . + implode(', ', $missingConfiguration) . ' value(s)'); + } + + // Initialize the logger class + if ($logger !== null) { + self::$logger = $logger; + } else { + self::$logger = new csrfpDefaultLogger(); + } + + // Authorise the incoming request + self::authorizePost(); + + // Initialize output buffering handler + if (!defined('__TESTING_CSRFP__')) { + ob_start('csrfProtector::ob_handler'); + } + + if (!isset($_COOKIE[self::$config['CSRFP_TOKEN']]) + || !isset($_SESSION[self::$config['CSRFP_TOKEN']]) + || !is_array($_SESSION[self::$config['CSRFP_TOKEN']]) + || !in_array($_COOKIE[self::$config['CSRFP_TOKEN']], + $_SESSION[self::$config['CSRFP_TOKEN']])) { + self::refreshToken(); + } + } + + /* + * Function: authorizePost + * function to authorise incoming post requests + * + * Parameters: + * void + * + * Returns: + * void + * + * TODO(mebjas): this method should be private. + */ + public static function authorizePost() + { + // TODO(mebjas): this method is valid for same origin request only, + // enable it for cross origin also sometime for cross origin the + // functionality is different. + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // Set request type to POST + self::$requestType = "POST"; + + // Look for token in payload else from header + $token = self::getTokenFromRequest(); + + // Currently for same origin only + if (!($token && isset($_SESSION[self::$config['CSRFP_TOKEN']]) + && (self::isValidToken($token)))) { + + // Action in case of failed validation + self::failedValidationAction(); + } else { + self::refreshToken(); //refresh token for successful validation + } + } else if (!static::isURLallowed()) { + // Currently for same origin only + if (!(isset($_GET[self::$config['CSRFP_TOKEN']]) + && isset($_SESSION[self::$config['CSRFP_TOKEN']]) + && (self::isValidToken($_GET[self::$config['CSRFP_TOKEN']])))) { + // Action in case of failed validation + self::failedValidationAction(); + } else { + self::refreshToken(); // Refresh token for successful validation + } + } + } + + /* + * Function: getTokenFromRequest + * function to get token in case of POST request + * + * Parameters: + * void + * + * Returns: + * any (string / bool) - token retrieved from header or form payload + */ + private static function getTokenFromRequest() + { + // Look for in $_POST, then header + if (isset($_POST[self::$config['CSRFP_TOKEN']])) { + return $_POST[self::$config['CSRFP_TOKEN']]; + } + + if (function_exists('getallheaders')) { + $requestHeaders = getallheaders(); + if (isset($requestHeaders[self::$config['CSRFP_TOKEN']])) { + return $requestHeaders[self::$config['CSRFP_TOKEN']]; + } + } + + if (self::$tokenHeaderKey === null) { + return false; + } + + if (isset($_SERVER[self::$tokenHeaderKey])) { + return $_SERVER[self::$tokenHeaderKey]; + } + + return false; + } + + /* + * Function: isValidToken + * function to check the validity of token in session array + * Function also clears all tokens older than latest one + * + * Parameters: + * $token - the token sent with GET or POST payload + * + * Returns: + * bool - true if its valid else false + */ + private static function isValidToken($token) + { + if (!isset($_SESSION[self::$config['CSRFP_TOKEN']])) { + return false; + } + + if (!is_array($_SESSION[self::$config['CSRFP_TOKEN']])) { + return false; + } + + foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $key => $value) { + if ($value == $token) { + // Clear all older tokens assuming they have been consumed + foreach ($_SESSION[self::$config['CSRFP_TOKEN']] as $_key => $_value) { + if ($_value == $token) break; + array_shift($_SESSION[self::$config['CSRFP_TOKEN']]); + } + + return true; + } + } + + return false; + } + + /* + * Function: failedValidationAction + * function to be called in case of failed validation + * performs logging and take appropriate action + * + * Parameters: + * void + * + * Returns: + * void + */ + private static function failedValidationAction() + { + //call the logging function + static::logCSRFattack(); + + // TODO(mebjas): ask mentors if $failedAuthAction is better as an int or string + // default case is case 0 + switch (self::$config['failedAuthAction'][self::$requestType]) { + case csrfpAction::ForbiddenResponseAction: + // Send 403 header + header('HTTP/1.0 403 Forbidden'); + exit("

403 Access Forbidden by CSRFProtector!

"); + break; + case csrfpAction::ClearParametersAction: + // Unset the query parameters and forward + if (self::$requestType === 'GET') { + $_GET = array(); + } else { + $_POST = array(); + } + break; + case csrfpAction::RedirectAction: + // Redirect to custom error page + $location = self::$config['errorRedirectionPage']; + header("location: $location"); + exit(self::$config['customErrorMessage']); + break; + case csrfpAction::CustomErrorMessageAction: + // Send custom error message + exit(self::$config['customErrorMessage']); + break; + case csrfpAction::InternalServerErrorResponseAction: + // Send 500 header -- internal server error + header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500); + exit("

500 Internal Server Error!

"); + break; + default: + // Unset the query parameters and forward + if (self::$requestType === 'GET') { + $_GET = array(); + } else { + $_POST = array(); + } + break; + } + } + + /* + * Function: refreshToken + * Function to set auth cookie + * + * Parameters: + * void + * + * Returns: + * void + */ + public static function refreshToken() + { + $token = self::generateAuthToken(); + + if (!isset($_SESSION[self::$config['CSRFP_TOKEN']]) + || !is_array($_SESSION[self::$config['CSRFP_TOKEN']])) + $_SESSION[self::$config['CSRFP_TOKEN']] = array(); + + // Set token to session for server side validation + array_push($_SESSION[self::$config['CSRFP_TOKEN']], $token); + + // Set token to cookie for client side processing + if (self::$cookieConfig === null) { + if (!isset(self::$config['cookieConfig'])) + self::$config['cookieConfig'] = array(); + self::$cookieConfig = new csrfpCookieConfig(self::$config['cookieConfig']); + } + + setcookie( + self::$config['CSRFP_TOKEN'], + $token, + time() + self::$cookieConfig->expire, + self::$cookieConfig->path, + self::$cookieConfig->domain, + (bool) self::$cookieConfig->secure); + } + + /* + * Function: generateAuthToken + * function to generate random hash of length as given in parameter + * max length = 128 + * + * Parameters: + * length to hash required, int + * + * Returns: + * string, token + */ + public static function generateAuthToken() + { + // TODO(mebjas): Make this a member method / configurable + $randLength = 64; + + // If config tokenLength value is 0 or some non int + if (intval(self::$config['tokenLength']) == 0) { + self::$config['tokenLength'] = 32; //set as default + } + + // TODO(mebjas): if $length > 128 throw exception + + if (function_exists("random_bytes")) { + $token = bin2hex(random_bytes($randLength)); + } elseif (function_exists("openssl_random_pseudo_bytes")) { + $token = bin2hex(openssl_random_pseudo_bytes($randLength)); + } else { + $token = ''; + for ($i = 0; $i < 128; ++$i) { + $r = mt_rand (0, 35); + if ($r < 26) { + $c = chr(ord('a') + $r); + } else { + $c = chr(ord('0') + $r - 26); + } + $token .= $c; + } + } + return substr($token, 0, self::$config['tokenLength']); + } + + /* + * Function: ob_handler + * Rewrites on the fly to add CSRF tokens to them. This can also + * inject our JavaScript library. + * + * Parameters: + * $buffer - output buffer to which all output are stored + * $flag - INT + * + * Return: + * string, complete output buffer + */ + public static function ob_handler($buffer, $flags) + { + // Even though the user told us to rewrite, we should do a quick heuristic + // to check if the page is *actually* HTML. We don't begin rewriting until + // we hit the first message to outgoing HTML output, + // informing the user to enable js for CSRFProtector to work + // best section to add, after tag + $buffer = preg_replace("/]*>/", "$0 ", $buffer); + + $hiddenInput = '' .PHP_EOL; + + $hiddenInput .= ''; + + // Implant hidden fields with check url information for reading in javascript + $buffer = str_ireplace('', $hiddenInput . '', $buffer); + + if (self::$config['jsUrl']) { + // Implant the CSRFGuard js file to outgoing script + $script = ''; + $buffer = str_ireplace('', $script . PHP_EOL . '', $buffer, $count); + + // Add the script to the end if the body tag was not closed + if (!$count) { + $buffer .= $script; + } + } + + return $buffer; + } + + /* + * Function: logCSRFattack + * Function to log CSRF Attack + * + * Parameters: + * void + * + * Returns: + * void + * + * Throws: + * logFileWriteError - if unable to log an attack + */ + protected static function logCSRFattack() + { + //miniature version of the log + $context = array(); + $context['HOST'] = $_SERVER['HTTP_HOST']; + $context['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + $context['requestType'] = self::$requestType; + $context['cookie'] = $_COOKIE; + self::$logger->log( + "OWASP CSRF PROTECTOR VALIDATION FAILURE", $context); + } + + /* + * Function: getCurrentUrl + * Function to return current url of executing page + * + * Parameters: + * void + * + * Returns: + * string - current url + */ + private static function getCurrentUrl() + { + $request_scheme = 'https'; + if (isset($_SERVER['REQUEST_SCHEME'])) { + $request_scheme = $_SERVER['REQUEST_SCHEME']; + } else { + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { + $request_scheme = 'https'; + } else { + $request_scheme = 'http'; + } + } + + return $request_scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; + } + + /* + * Function: isURLallowed + * Function to check if a url matches for any urls + * Listed in config file + * + * Parameters: + * void + * + * Returns: + * boolean - true is url need no validation, false if validation needed + */ + public static function isURLallowed() { + foreach (self::$config['verifyGetFor'] as $key => $value) { + $value = str_replace(array('/','*'), array('\/','(.*)'), $value); + preg_match('/' .$value .'/', self::getCurrentUrl(), $output); + if (count($output) > 0) { + return false; + } + } + + return true; + } + }; +} diff --git a/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/index.php b/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/index.php new file mode 100644 index 000000000..6fab721c9 --- /dev/null +++ b/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/index.php @@ -0,0 +1,7 @@ + "", + "failedAuthAction" => array( + "GET" => 0, + "POST" => 0), + "errorRedirectionPage" => "", + "customErrorMessage" => "", + "jsUrl" => $config['url'] .'/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js', + "tokenLength" => 10, + "cookieConfig" => array( + "path" => '', + "domain" => '', + "secure" => true, + "expire" => '', + ), + "disabledJavascriptMessage" => "This site attempts to protect users against + Cross-Site Request Forgeries attacks. In order to do so, you must have JavaScript enabled in your web browser otherwise this site will fail to work correctly for you. + See details of your web browser for how to enable JavaScript.", + "verifyGetFor" => array() +); From 3a7cb0b5cda2190c80fd012724eb97c7c478d099 Mon Sep 17 00:00:00 2001 From: Ben Nottelling Date: Fri, 6 Aug 2021 19:16:15 -0700 Subject: [PATCH 2/4] Because not everyone run an HTTPS only site, disable secure cookies by default --- src/bb-config-sample.php | 3 +++ src/csrfp-config.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bb-config-sample.php b/src/bb-config-sample.php index 0dac096a6..d1cd37700 100755 --- a/src/bb-config-sample.php +++ b/src/bb-config-sample.php @@ -19,6 +19,9 @@ */ 'url' => 'http://localhost/', + /** + * The URL prefix to access the BB admin area. Ex: '/bb-admin' = https://example.com/bb-admin + */ 'admin_area_prefix' => '/bb-admin', /** diff --git a/src/csrfp-config.php b/src/csrfp-config.php index 774b57dc6..1e52353ba 100644 --- a/src/csrfp-config.php +++ b/src/csrfp-config.php @@ -18,7 +18,7 @@ "cookieConfig" => array( "path" => '', "domain" => '', - "secure" => true, + "secure" => false, "expire" => '', ), "disabledJavascriptMessage" => "This site attempts to protect users against From 30809748cd8e60a397375dc8ab30014d54b4a620 Mon Sep 17 00:00:00 2001 From: Benjamin Aerni Date: Sat, 7 Aug 2021 09:33:39 -0700 Subject: [PATCH 3/4] Remove unneeded breaks and sync JS file --- .../CSRF-Protector-PHP/js/csrfprotector.js | 290 +++++++++--------- .../libs/csrf/csrfprotector.php | 4 - 2 files changed, 139 insertions(+), 155 deletions(-) diff --git a/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js b/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js index 84816f0fe..ac58c556b 100644 --- a/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js +++ b/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js @@ -24,15 +24,12 @@ var CSRFP = { */ checkForUrls: [], /** - * Function to check if a certain url is allowed to perform the request - * With or without csrf token + * Returns true if the get request doesn't need csrf token. * - * @param {string} url - * - * @return {Boolean} true if csrftoken is not needed - * false if csrftoken is needed + * @param {String} url to check. + * @return {Boolean} true if csrftoken is not needed. */ - _isValidGetRequest: function(url) { + _isValidGetRequest: function (url) { for (var i = 0; i < CSRFP.checkForUrls.length; i++) { var match = CSRFP.checkForUrls[i].exec(url); if (match !== null && match.length > 0) { @@ -42,141 +39,133 @@ var CSRFP = { return true; }, /** - * Function to get Auth key from cookie and return it to requesting function - * - * @param: void + * Returns auth key from cookie. * - * @return {string|Boolean} csrftoken retrieved from cookie + * @return {String} auth key from cookie. */ - _getAuthKey: function() { - var re = new RegExp(CSRFP.CSRFP_TOKEN +"=([^;]+)(;|$)"); - var RegExpArray = re.exec(document.cookie); - - if (RegExpArray === null) { - return false; + _getAuthKey: function () { + var regex = new RegExp(`(?:^|;\s*)${CSRFP.CSRFP_TOKEN}=([^;]+)(;|$)`); + var regexResult = regex.exec(document.cookie); + if (regexResult === null) { + return null; } - return RegExpArray[1]; + + return regexResult[1]; }, /** - * Function to get domain of any url - * - * @param {string} url + * Returns domain name of a url. * - * @return {string} domain of url + * @param {String} url - url to check. + * @return {String} domain of the input url. */ - _getDomain: function(url) { - if (url.indexOf("http://") !== 0 - && url.indexOf("https://") !== 0) + _getDomain: function (url) { + // TODO(mebjas): add support for other protocols that web supports. + if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) { return document.domain; + } return /http(s)?:\/\/([^\/]+)/.exec(url)[2]; }, /** - * Function to create and return a hidden input element - * For storing the CSRFP_TOKEN - * - * @param: void + * Creates hidden input element with CSRF_TOKEN in it. * - * @return {HTMLInputElement} input element + * @return {HTMLInputElement} hidden input element. */ - _getInputElt: function() { - var hiddenObj = document.createElement("input"); - hiddenObj.setAttribute('name', CSRFP.CSRFP_TOKEN); - hiddenObj.setAttribute('class', CSRFP.CSRFP_TOKEN); - hiddenObj.type = 'hidden'; - hiddenObj.value = CSRFP._getAuthKey(); - return hiddenObj; + _createHiddenInputElement: function () { + var inputElement = document.createElement('input'); + inputElement.setAttribute('name', CSRFP.CSRFP_TOKEN); + inputElement.setAttribute('class', CSRFP.CSRFP_TOKEN); + inputElement.type = 'hidden'; + inputElement.value = CSRFP._getAuthKey(); + return inputElement; }, /** - * Returns absolute path for relative path + * Returns absolute url from the input relative components. * - * @param {string} base base url - * @param {string} relative relative url - * - * @return {string} absolute path + * @param {String} basePart - base part of the url. + * @param {String} relativePart - relative part of the url. + * @return {String} absolute url. */ - _getAbsolutePath: function(base, relative) { - var stack = base.split("/"); - var parts = relative.split("/"); - // remove current file name (or empty string) - // (omit if "base" is the current folder without trailing slash) - stack.pop(); - + _createAbsolutePath: function (basePart, relativePart) { + var stack = basePart.split("/"); + var parts = relativePart.split("/"); + stack.pop(); + for (var i = 0; i < parts.length; i++) { - if (parts[i] === ".") + if (parts[i] === ".") { continue; - if (parts[i] === "..") + } + if (parts[i] === "..") { stack.pop(); - else + } else { stack.push(parts[i]); + } } return stack.join("/"); }, /** - * Remove jcsrfp-token run fun and then put them back - * - * @param {function} fun - * @param {object} obj reference form obj + * Creates a function wrapper around {@param runnableFunction}, removes + * CSRF Token before calling the function and then put it back. * - * @return function + * @param {Function} runnableFunction - function to run. + * @param {Object} htmlFormObject - reference form object. + * @return modified wrapped function. */ - _csrfpWrap: function(fun, obj) { - return function(event) { + _createCsrfpWrappedFunction: function (runnableFunction, htmlFormObject) { + return function (event) { // Remove CSRf token if exists - if (typeof obj[CSRFP.CSRFP_TOKEN] !== 'undefined') { - var target = obj[CSRFP.CSRFP_TOKEN]; + if (typeof htmlFormObject[CSRFP.CSRFP_TOKEN] !== 'undefined') { + var target = htmlFormObject[CSRFP.CSRFP_TOKEN]; target.parentNode.removeChild(target); } - + // Trigger the functions - var result = fun.apply(this, [event]); - + var result = runnableFunction.apply(this, [event]); + // Now append the CSRFP-Token back - obj.appendChild(CSRFP._getInputElt()); - + htmlFormObject.appendChild(CSRFP._createHiddenInputElement()); return result; }; }, /** - * Initialises the CSRFProtector js script - * - * @param: void - * - * @return void + * Initialises the CSRFProtector js script. */ - _init: function() { - CSRFP.CSRFP_TOKEN = document.getElementById(CSRFP_FIELD_TOKEN_NAME).value; + _init: function () { + this.CSRFP_TOKEN = document.getElementById( + CSRFP_FIELD_TOKEN_NAME).value; + try { - CSRFP.checkForUrls = JSON.parse(document.getElementById(CSRFP_FIELD_URLS).value); - } catch (err) { - console.error(err); - console.error('[ERROR] [CSRF Protector] unable to parse blacklisted url fields.'); + var csrfFieldElem = document.getElementById(CSRFP_FIELD_URLS); + this.checkForUrls = JSON.parse(csrfFieldElem.value); + } catch (exception) { + console.error(exception); + console.error('[ERROR] [CSRF Protector] unable to parse blacklisted' + + ` url fields. Exception = ${exception}`); } - //convert these rules received from php lib to regex objects + // Convert the rules received from php library to regex objects. for (var i = 0; i < CSRFP.checkForUrls.length; i++) { - CSRFP.checkForUrls[i] = CSRFP.checkForUrls[i].replace(/\*/g, '(.*)') - .replace(/\//g, "\\/"); - CSRFP.checkForUrls[i] = new RegExp(CSRFP.checkForUrls[i]); + this.checkForUrls[i] + = this.checkForUrls[i].replace(/\*/g, '(.*)') + .replace(/\//g, "\\/"); + this.checkForUrls[i] = new RegExp(CSRFP.checkForUrls[i]); } - } - -}; +} //========================================================== // Adding tokens, wrappers on window onload //========================================================== function csrfprotector_init() { - + // Call the init function CSRFP._init(); - // definition of basic FORM submit event handler to intercept the form request - // and attach a CSRFP TOKEN if it's not already available - var BasicSubmitInterceptor = function(event) { - if (typeof event.target[CSRFP.CSRFP_TOKEN] === 'undefined') { - event.target.appendChild(CSRFP._getInputElt()); + // Basic FORM submit event handler to intercept the form request and attach + // a CSRFP TOKEN if it's not already available. + var basicSubmitInterceptor = function (event) { + if (!event.target[CSRFP.CSRFP_TOKEN]) { + event.target.appendChild(CSRFP._createHiddenInputElement()); } else { //modify token to latest value event.target[CSRFP.CSRFP_TOKEN].value = CSRFP._getAuthKey(); @@ -189,44 +178,41 @@ function csrfprotector_init() { // TODO - check for method //================================================================== // run time binding - document.querySelector('body').addEventListener('submit', function(event) { + document.querySelector('body').addEventListener('submit', function (event) { if (event.target.tagName.toLowerCase() === 'form') { - BasicSubmitInterceptor(event); + basicSubmitInterceptor(event); } }); - // initial binding - // for(var i = 0; i < document.forms.length; i++) { - // document.forms[i].addEventListener("submit", BasicSubmitInterceptor); - // } - //================================================================== // Adding csrftoken to request resulting from direct form.submit() call // Add for each POST, while for mentioned GET request // TODO - check for form method //================================================================== - HTMLFormElement.prototype.submit_ = HTMLFormElement.prototype.submit; - HTMLFormElement.prototype.submit = function() { + HTMLFormElement.prototype.submit_real = HTMLFormElement.prototype.submit; + HTMLFormElement.prototype.submit = function () { // check if the FORM already contains the token element - if (!this.getElementsByClassName(CSRFP.CSRFP_TOKEN).length) - this.appendChild(CSRFP._getInputElt()); - this.submit_(); + if (!this.getElementsByClassName(CSRFP.CSRFP_TOKEN).length) { + this.appendChild(CSRFP._createHiddenInputElement()); + } + this.submit_real(); }; - /** * Add wrapper for HTMLFormElements addEventListener so that any further * addEventListens won't have trouble with CSRF token * todo - check for method */ - HTMLFormElement.prototype.addEventListener_ = HTMLFormElement.prototype.addEventListener; - HTMLFormElement.prototype.addEventListener = function(eventType, fun, bubble) { + HTMLFormElement.prototype.addEventListener_real + = HTMLFormElement.prototype.addEventListener; + HTMLFormElement.prototype.addEventListener = function ( + eventType, func, bubble) { if (eventType === 'submit') { - var wrapped = CSRFP._csrfpWrap(fun, this); - this.addEventListener_(eventType, wrapped, bubble); + var wrappedFunc = CSRFP._createCsrfpWrappedFunction(func, this); + this.addEventListener_real(eventType, wrappedFunc, bubble); } else { - this.addEventListener_(eventType, fun, bubble); - } + this.addEventListener_real(eventType, func, bubble); + } }; /** @@ -234,19 +220,19 @@ function csrfprotector_init() { * todo - check for method * todo - typeof is now obsolete for IE 11, use some other method. */ - if (typeof HTMLFormElement.prototype.attachEvent !== 'undefined') { - HTMLFormElement.prototype.attachEvent_ = HTMLFormElement.prototype.attachEvent; - HTMLFormElement.prototype.attachEvent = function(eventType, fun) { + if (HTMLFormElement.prototype.attachEvent) { + HTMLFormElement.prototype.attachEvent_real + = HTMLFormElement.prototype.attachEvent; + HTMLFormElement.prototype.attachEvent = function (eventType, func) { if (eventType === 'onsubmit') { - var wrapped = CSRFP._csrfpWrap(fun, this); - this.attachEvent_(eventType, wrapped); + var wrappedFunc = CSRFP._createCsrfpWrappedFunction(func, this); + this.attachEvent_real(eventType, wrappedFunc); } else { - this.attachEvent_(eventType, fun); + this.attachEvent_real(eventType, func); } } } - //================================================================== // Wrapper for XMLHttpRequest & ActiveXObject (for IE 6 & below) // Set X-No-CSRF to true before sending if request method is @@ -260,19 +246,19 @@ function csrfprotector_init() { */ function new_open(method, url, async, username, password) { this.method = method; - var isAbsolute = (url.indexOf("./") === -1); + var isAbsolute = url.indexOf("./") === -1; if (!isAbsolute) { - var base = location.protocol +'//' +location.host - + location.pathname; - url = CSRFP._getAbsolutePath(base, url); + var base = location.protocol + '//' + location.host + + location.pathname; + url = CSRFP._createAbsolutePath(base, url); } - if (method.toLowerCase() === 'get' - && !CSRFP._isValidGetRequest(url)) { - //modify the url + + if (method.toLowerCase() === 'get' && !CSRFP._isValidGetRequest(url)) { + var token = CSRFP._getAuthKey(); if (url.indexOf('?') === -1) { - url += "?" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + url += `?${CSRFP.CSRFP_TOKEN}=${token}` } else { - url += "&" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + url += `&${CSRFP.CSRFP_TOKEN}=${token}`; } } @@ -306,7 +292,7 @@ function csrfprotector_init() { ActiveXObject.prototype.old_send = ActiveXObject.prototype.send; ActiveXObject.prototype.old_open = ActiveXObject.prototype.open; ActiveXObject.prototype.open = new_open; - ActiveXObject.prototype.send = new_send; + ActiveXObject.prototype.send = new_send; } //================================================================== // Rewrite existing urls ( Attach CSRF token ) @@ -318,42 +304,44 @@ function csrfprotector_init() { //================================================================== for (var i = 0; i < document.links.length; i++) { - document.links[i].addEventListener("mousedown", function(event) { + document.links[i].addEventListener("mousedown", function (event) { var href = event.target.href; - if(typeof href === "string") - { - var urlParts = href.split('#'); - var url = urlParts[0]; - var hash = urlParts[1]; + if (typeof href !== "string") { + return; + } + var urlParts = href.split('#'); + var url = urlParts[0]; + var hash = urlParts[1]; - if(CSRFP._getDomain(url).indexOf(document.domain) === -1 - || CSRFP._isValidGetRequest(url)) { - //cross origin or not to be protected by rules -- ignore - return; - } + if (CSRFP._getDomain(url).indexOf(document.domain) === -1 + || CSRFP._isValidGetRequest(url)) { + //cross origin or not to be protected by rules -- ignore + return; + } - if (url.indexOf('?') !== -1) { - if(url.indexOf(CSRFP.CSRFP_TOKEN) === -1) { - url += "&" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); - } else { - url = url.replace(new RegExp(CSRFP.CSRFP_TOKEN +"=.*?(&|$)", 'g'), - CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey() + "$1"); - } + var token = CSRFP._getAuthKey(); + if (url.indexOf('?') !== -1) { + if (url.indexOf(CSRFP.CSRFP_TOKEN) === -1) { + url += `&${CSRFP.CSRFP_TOKEN}=${token}`; } else { - url += "?" +CSRFP.CSRFP_TOKEN +"=" +CSRFP._getAuthKey(); + var replacementString = `${CSRFP.CSRFP_TOKEN}=${token}$1`; + url = url.replace( + new RegExp(CSRFP.CSRFP_TOKEN + "=.*?(&|$)", 'g'), + replacementString); } + } else { + url += `?${CSRFP.CSRFP_TOKEN}=${token}`; + } - event.target.href = url; - if (typeof hash !== 'undefined') { - event.target.href += '#' +hash; - } + event.target.href = url; + if (hash) { + event.target.href += `#${hash}`; } }); } - } -window.addEventListener("DOMContentLoaded", function() { +window.addEventListener("DOMContentLoaded", function () { csrfprotector_init(); // Dispatch an event so clients know the library has initialized diff --git a/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfprotector.php b/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfprotector.php index 6df0b1692..3be213b5c 100644 --- a/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfprotector.php +++ b/src/bb-library/Security/CSRF-Protector-PHP/libs/csrf/csrfprotector.php @@ -348,7 +348,6 @@ private static function failedValidationAction() // Send 403 header header('HTTP/1.0 403 Forbidden'); exit("

403 Access Forbidden by CSRFProtector!

"); - break; case csrfpAction::ClearParametersAction: // Unset the query parameters and forward if (self::$requestType === 'GET') { @@ -362,16 +361,13 @@ private static function failedValidationAction() $location = self::$config['errorRedirectionPage']; header("location: $location"); exit(self::$config['customErrorMessage']); - break; case csrfpAction::CustomErrorMessageAction: // Send custom error message exit(self::$config['customErrorMessage']); - break; case csrfpAction::InternalServerErrorResponseAction: // Send 500 header -- internal server error header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error', true, 500); exit("

500 Internal Server Error!

"); - break; default: // Unset the query parameters and forward if (self::$requestType === 'GET') { From a5e35b8b7f593830563edcd562747104ccc3b9d6 Mon Sep 17 00:00:00 2001 From: Benjamin Aerni Date: Sat, 7 Aug 2021 09:43:32 -0700 Subject: [PATCH 4/4] Fix missing slash --- src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js b/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js index ac58c556b..f2bb8d477 100644 --- a/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js +++ b/src/bb-library/Security/CSRF-Protector-PHP/js/csrfprotector.js @@ -44,7 +44,7 @@ var CSRFP = { * @return {String} auth key from cookie. */ _getAuthKey: function () { - var regex = new RegExp(`(?:^|;\s*)${CSRFP.CSRFP_TOKEN}=([^;]+)(;|$)`); + var regex = new RegExp((?:^|;\\s*)${CSRFP.CSRFP_TOKEN}=([^;]+)(;|$)); var regexResult = regex.exec(document.cookie); if (regexResult === null) { return null;