*/ $GLOBALS['csrf']['input-name'] = '__csrf_magic'; /** * Set this to false if your site must work inside of frame/iframe elements, * but do so at your own risk: this configuration protects you against CSS * overlay attacks that defeat tokens. */ $GLOBALS['csrf']['frame-breaker'] = true; /** * Whether or not CSRF Magic should be allowed to start a new session in order * to determine the key. */ $GLOBALS['csrf']['auto-session'] = true; /** * Whether or not csrf-magic should produce XHTML style tags. */ $GLOBALS['csrf']['xhtml'] = true; // FUNCTIONS: // Don't edit this! $GLOBALS['csrf']['version'] = '1.0.4'; /** * Rewrites
on the fly to add CSRF tokens to them. This can also * inject our JavaScript library. */ function csrf_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 "; $buffer = preg_replace('#(]*method\s*=\s*["\']post["\'][^>]*>)#i', '$1' . $input, $buffer); if ($GLOBALS['csrf']['frame-breaker']) { $buffer = str_ireplace('', ' ', $buffer); } if ($js = $GLOBALS['csrf']['rewrite-js']) { $buffer = str_ireplace( '', ''. ' ', $buffer ); $script = ''; $buffer = str_ireplace('', $script . '', $buffer, $count); if (!$count) { $buffer .= $script; } } return $buffer; } /** * Checks if this is a post request, and if it is, checks if the nonce is valid. * @param bool $fatal Whether or not to fatally error out if there is a problem. * @return True if check passes or is not necessary, false if failure. */ function csrf_check($fatal = true) { if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; global $cspNonce; csrf_start(); $name = $GLOBALS['csrf']['input-name']; $ok = false; $tokens = ''; do { if (!isset($_POST[$name])) { #Logger::Debug("POST[$name] is not set"); break; #} else { #Logger::Debug("POST[$name] is set as " . $_POST[$name] ); } // we don't regenerate a token and check it because some token creation // schemes are volatile. $tokens = $_POST[$name]; if (!csrf_check_tokens($tokens)) { #Logger::Debug("Failed checking tokens"); break; #} else { #Logger::Debug("Token passed"); } $ok = true; } while (false); if ($fatal && !$ok) { $callback = $GLOBALS['csrf']['callback']; if (trim($tokens, 'A..Za..z0..9:;,') !== '') $tokens = 'hidden'; $callback($tokens); exit; } return $ok; } /** * Retrieves a valid token(s) for a particular context. Tokens are separated * by semicolons. */ function csrf_get_tokens() { $has_cookies = !empty($_COOKIE); // $ip implements a composite key, which is sent if the user hasn't sent // any cookies. It may or may not be used, depending on whether or not // the cookies "stick" $secret = csrf_get_secret(); if (!$has_cookies && $secret) { // :TODO: Harden this against proxy-spoofing attacks $IP_ADDRESS = (isset($_SERVER['IP_ADDRESS']) ? $_SERVER['IP_ADDRESS'] : $_SERVER['REMOTE_ADDR']); $ip = ';ip:' . csrf_hash($IP_ADDRESS); } else { $ip = ''; } csrf_start(); // These are "strong" algorithms that don't require per se a secret if ($GLOBALS['csrf']['key']) return 'key:' . csrf_hash($GLOBALS['csrf']['key']) . $ip; if (session_id()) return 'sid:' . csrf_hash(session_id()) . $ip; if ($GLOBALS['csrf']['cookie']) { $val = csrf_generate_secret(); setcookie($GLOBALS['csrf']['cookie'], $val); return 'cookie:' . csrf_hash($val) . $ip; } // These further algorithms require a server-side secret if (!$secret) return 'invalid'; if ($GLOBALS['csrf']['user'] !== false) { return 'user:' . csrf_hash($GLOBALS['csrf']['user']); } if ($GLOBALS['csrf']['allow-ip']) { return ltrim($ip, ';'); } return 'invalid'; } function csrf_flattenpost($data) { $ret = array(); foreach($data as $n => $v) { $ret = array_merge($ret, csrf_flattenpost2(1, $n, $v)); } return $ret; } function csrf_flattenpost2($level, $key, $data) { if(!is_array($data)) return array($key => $data); $ret = array(); foreach($data as $n => $v) { $nk = $level >= 1 ? $key."[$n]" : "[$n]"; $ret = array_merge($ret, csrf_flattenpost2($level+1, $nk, $v)); } return $ret; } /** * @param $tokens is safe for HTML consumption */ function csrf_callback($tokens) { // (yes, $tokens is safe to echo without escaping) header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); $data = ''; foreach (csrf_flattenpost($_POST) as $key => $value) { if ($key == $GLOBALS['csrf']['input-name']) continue; $data .= ''; } echo "CSRF check failed

CSRF check failed. Your form session may have expired, or you may not have cookies enabled.

"; if (ZM_LOG_DEBUG) { // Don't make it too easy for users to inflict a CSRF attack on themselves. echo "

Only try again if you weren't sent to this page by someone as this is potentially a sign of an attack.

"; echo "$data"; ZM\Logger::Debug("Failed csrf check"); } echo "

Debug: $tokens

"; } /** * Checks if a composite token is valid. Outward facing code should use this * instead of csrf_check_token() */ function csrf_check_tokens($tokens) { if (is_string($tokens)) $tokens = explode(';', $tokens); foreach ($tokens as $token) { if (csrf_check_token($token)) return true; } return false; } /** * Checks if a token is valid. */ function csrf_check_token($token) { #Logger::Debug("Checking CSRF token $token"); if (strpos($token, ':') === false) { #Logger::Debug("Checking CSRF token $token bad because no :"); return false; } list($type, $value) = explode(':', $token, 2); if (strpos($value, ',') === false) { #Logger::Debug("Checking CSRF token $token bad because no ,"); return false; } list($x, $time) = explode(',', $token, 2); if ($GLOBALS['csrf']['expires']) { if (time() > $time + $GLOBALS['csrf']['expires']) { #Logger::Debug("Checking CSRF token $token bad because expired"); return false; } } switch ($type) { case 'sid': { #Logger::Debug("Checking sid: $value === " . csrf_hash(session_id(), $time) ); return $value === csrf_hash(session_id(), $time); } case 'cookie': $n = $GLOBALS['csrf']['cookie']; if (!$n) return false; if (!isset($_COOKIE[$n])) return false; return $value === csrf_hash($_COOKIE[$n], $time); case 'key': if (!$GLOBALS['csrf']['key']) { Logger::Debug("Checking key: no key set" ); return false; } #Logger::Debug("Checking sid: $value === " . csrf_hash($GLOBALS['csrf']['key'], $time) ); return $value === csrf_hash($GLOBALS['csrf']['key'], $time); // We could disable these 'weaker' checks if 'key' was set, but // that doesn't make me feel good then about the cookie-based // implementation. case 'user': if (!csrf_get_secret()) return false; if ($GLOBALS['csrf']['user'] === false) return false; return $value === csrf_hash($GLOBALS['csrf']['user'], $time); case 'ip': if (!csrf_get_secret()) return false; // do not allow IP-based checks if the username is set, or if // the browser sent cookies if ($GLOBALS['csrf']['user'] !== false) return false; if (!empty($_COOKIE)) return false; if (!$GLOBALS['csrf']['allow-ip']) return false; $IP_ADDRESS = (isset($_SERVER['IP_ADDRESS']) ? $_SERVER['IP_ADDRESS'] : $_SERVER['REMOTE_ADDR']); return $value === csrf_hash($IP_ADDRESS, $time); } return false; } /** * Sets a configuration value. */ function csrf_conf($key, $val) { if (!isset($GLOBALS['csrf'][$key])) { trigger_error('No such configuration ' . $key, E_USER_WARNING); return; } $GLOBALS['csrf'][$key] = $val; } /** * Starts a session if we're allowed to. */ function csrf_start() { if ($GLOBALS['csrf']['auto-session'] && !session_id()) { zm_session_start(); } } /** * Retrieves the secret, and generates one if necessary. */ function csrf_get_secret() { if ($GLOBALS['csrf']['secret']) return $GLOBALS['csrf']['secret']; $dir = dirname(__FILE__); $file = $dir . '/csrf-secret.php'; $secret = ''; if (file_exists($file)) { include $file; return $secret; } if (is_writable($dir)) { $secret = csrf_generate_secret(); $fh = fopen($file, 'w'); fwrite($fh, '