PHP , $this->buildRuleSet($rules)), 'rules'); if (!empty($ruleString) && WFWAF_DEBUG && !file_exists($this->getStorageEngine()->getRulesDSLCacheFile())) { wfWAFStorageFile::atomicFilePutContents($this->getStorageEngine()->getRulesDSLCacheFile(), $ruleString, 'rules'); } } else { $rules = $this->_preprocessRulesArray($rules); $this->getStorageEngine()->setRules($rules); } if ($updateLastUpdatedTimestamp) { $this->getStorageEngine()->setConfig('rulesLastUpdated', is_int($updateLastUpdatedTimestamp) ? $updateLastUpdatedTimestamp : time(), 'transient'); } } catch (wfWAFBuildRulesException $e) { // Do something. throw $e; } } /** * @param string|array $rules * @return string * @throws wfWAFException */ public function buildRuleSet($rules) { if (is_string($rules)) { $parser = new wfWAFRuleParser(new wfWAFRuleLexer($rules), $this); $rules = $parser->parse(); } if (!array_key_exists('rules', $rules) || !is_array($rules['rules'])) { throw new wfWAFBuildRulesException('Invalid rule format passed to buildRuleSet.'); } $exportedCode = ''; $rules = $this->_preprocessRulesArray($rules); if (isset($rules['scores']) && is_array($rules['scores'])) { foreach ($rules['scores'] as $category => $score) { $exportedCode .= sprintf("\$this->failScores[%s] = %d;\n", var_export($category, true), $score); } $exportedCode .= "\n"; } if (isset($rules['variables']) && is_array($rules['variables'])) { foreach ($rules['variables'] as $var => $value) { $exportedCode .= sprintf("\$this->variables[%s] = %s;\n", var_export($var, true), ($value instanceof wfWAFRuleVariable) ? $value->render() : var_export($value, true)); } $exportedCode .= "\n"; } foreach (array('blacklistedParams', 'whitelistedParams') as $key) { if (isset($rules[$key]) && is_array($rules[$key])) { foreach ($rules[$key] as $paramKey => $payloads) { foreach ($payloads as $payload) { /** @var array|string $payload */ if (is_string($payload)) { $exportedCode .= sprintf("\$this->{$key}[%s][] = %s;\n", var_export($paramKey, true), var_export($payload, true) ); } else if (array_key_exists('conditional', $payload)) { $exportedCode .= sprintf("\$this->{$key}[%s][] = array(\n%s => %s,\n%s => %s,\n%s => %s\n);\n", var_export($paramKey, true), var_export('url', true), var_export($payload['url'], true), var_export('rules', true), var_export($payload['rules'], true), var_export('conditional', true), $payload['conditional']->render() ); } else { $exportedCode .= sprintf("\$this->{$key}[%s][] = array(\n%s => %s,\n%s => %s,\n);\n", var_export($paramKey, true), var_export('url', true), var_export($payload['url'], true), var_export('rules', true), var_export($payload['rules'], true) ); } } } $exportedCode .= "\n"; } } /** @var wfWAFRule $rule */ foreach ($rules['rules'] as $rule) { $rule->setWAF($this); $exportedCode .= sprintf(<<rules[%d] = %s; HTML , $rule->getRuleID(), $rule->render() ); } return $exportedCode; } /** * Preprocesses the parsed rules array so it's suitable for use both by the file-based storage engine and the mysqli * engine. * * @param array $rules * @return array */ protected function _preprocessRulesArray($rules) { $cleaned = $rules; foreach (array('blacklistedParams', 'whitelistedParams') as $key) { if (isset($rules[$key]) && is_array($rules[$key])) { /** @var wfWAFRuleParserURLParam $urlParam */ foreach ($rules[$key] as $index => $urlParam) { unset($cleaned[$key][$index]); $paramKey = $urlParam->getParam(); if (!array_key_exists($paramKey, $cleaned[$key])) { $cleaned[$key][$paramKey] = array(); } if ($urlParam->getConditional()) { $cleaned[$key][$paramKey][] = array( 'url' => $urlParam->getUrl(), 'rules' => $urlParam->getRules(), 'conditional' => $urlParam->getConditional(), ); } else if ($urlParam->getRules()) { $cleaned[$key][$paramKey][] = array( 'url' => $urlParam->getUrl(), 'rules' => $urlParam->getRules(), ); } else { $cleaned[$key][$paramKey][] = $urlParam->getUrl(); } } } } return $cleaned; } /** * @param $rules * @return wfWAFRuleComparisonGroup * @throws wfWAFBuildRulesException */ protected function _buildRuleSet($rules) { $ruleGroup = new wfWAFRuleComparisonGroup(); foreach ($rules as $rule) { if (!array_key_exists('type', $rule)) { throw new wfWAFBuildRulesException('Invalid rule: type not set.'); } switch ($rule['type']) { case 'comparison_group': if (!array_key_exists('comparisons', $rule) || !is_array($rule['comparisons'])) { throw new wfWAFBuildRulesException('Invalid rule format passed to _buildRuleSet.'); } $ruleGroup->add($this->_buildRuleSet($rule['comparisons'])); break; case 'comparison': if (array_key_exists('parameter', $rule)) { $rule['parameters'] = array($rule['parameter']); } foreach (array('action', 'expected', 'parameters') as $ruleRequirement) { if (!array_key_exists($ruleRequirement, $rule)) { throw new wfWAFBuildRulesException("Invalid rule: $ruleRequirement not set."); } } $ruleGroup->add(new wfWAFRuleComparison($this, $rule['action'], $rule['expected'], $rule['parameters'])); break; case 'operator': if (!array_key_exists('operator', $rule)) { throw new wfWAFBuildRulesException('Invalid rule format passed to _buildRuleSet. operator not passed.'); } $ruleGroup->add(new wfWAFRuleLogicalOperator($rule['operator'])); break; default: throw new wfWAFBuildRulesException("Invalid rule type [{$rule['type']}] passed to _buildRuleSet."); } } return $ruleGroup; } public function isRuleDisabled($ruleID) { if ($this->disabledRules === null) { $this->disabledRules = $this->getStorageEngine()->getConfig('disabledRules'); if (!is_array($this->disabledRules)) { $this->disabledRules = array(); } } return !empty($this->disabledRules[$ruleID]); } public function getDisabledRuleIDs() { if ($this->disabledRules === null) { $this->disabledRules = $this->getStorageEngine()->getConfig('disabledRules'); if (!is_array($this->disabledRules)) { $this->disabledRules = array(); } } $ruleIDs = array(); foreach ($this->disabledRules as $id => $value) { if (!empty($value)) { $ruleIDs[] = $id; } } return $ruleIDs; } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @throws wfWAFBlockException */ public function fail($rule, $failedComparison) { $category = $rule->getCategory(); $paramKey = $failedComparison->getParamKey(); $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'block', ); } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @throws wfWAFBlockException */ public function failXSS($rule, $failedComparison) { $category = $rule->getCategory(); $paramKey = $failedComparison->getParamKey(); $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'blockXSS', ); } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @throws wfWAFBlockException */ public function failSQLi($rule, $failedComparison) { $category = $rule->getCategory(); $paramKey = $failedComparison->getParamKey(); $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'blockSQLi', ); } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @throws wfWAFAllowException */ public function allow($rule, $failedComparison) { // Exclude this request from further blocking $e = new wfWAFAllowException(); $e->setFailedRules(array($rule)); $e->setParamKey($failedComparison->getParamKey()); $e->setParamValue($failedComparison->getParamValue()); $e->setRequest($this->getRequest()); throw $e; } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @param bool $updateFailedRules * @throws wfWAFBlockException */ public function block($rule, $failedComparison, $updateFailedRules = true) { $paramKey = $failedComparison->getParamKey(); $category = $rule->getCategory(); if ($updateFailedRules) { $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'block', ); } $e = new wfWAFBlockException(); $e->setFailedRules(array($rule)); $e->setParamKey($failedComparison->getParamKey()); $e->setParamValue($failedComparison->getParamValue()); $e->setRequest($this->getRequest()); throw $e; } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @param bool $updateFailedRules * @throws wfWAFBlockXSSException */ public function blockXSS($rule, $failedComparison, $updateFailedRules = true) { $paramKey = $failedComparison->getParamKey(); $category = $rule->getCategory(); if ($updateFailedRules) { $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'blockXSS', ); } $e = new wfWAFBlockXSSException(); $e->setFailedRules(array($rule)); $e->setParamKey($failedComparison->getParamKey()); $e->setParamValue($failedComparison->getParamValue()); $e->setRequest($this->getRequest()); throw $e; } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @param bool $updateFailedRules * @throws wfWAFBlockSQLiException */ public function blockSQLi($rule, $failedComparison, $updateFailedRules = true) { // Verify the param looks like SQLi to help reduce false positives. if (!wfWAFSQLiParser::testForSQLi($failedComparison->getParamValue())) { return; } $paramKey = $failedComparison->getParamKey(); $category = $rule->getCategory(); if ($updateFailedRules) { $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'blockXSS', ); } $e = new wfWAFBlockSQLiException(); $e->setFailedRules(array($rule)); $e->setParamKey($failedComparison->getParamKey()); $e->setParamValue($failedComparison->getParamValue()); $e->setRequest($this->getRequest()); throw $e; } /** * @param wfWAFRule $rule * @param wfWAFRuleComparisonFailure $failedComparison * @param bool $updateFailedRules */ public function log($rule, $failedComparison, $updateFailedRules = true) { $paramKey = $failedComparison->getParamKey(); $category = $rule->getCategory(); if ($updateFailedRules) { $this->failedRules[$paramKey][$category][] = array( 'rule' => $rule, 'failedComparison' => $failedComparison, 'action' => 'log', ); } $event=new wfWAFLogEvent( array($rule), $failedComparison->getParamKey(), $failedComparison->getParamValue(), $this->getRequest() ); $this->recordLogEvent($event); } public function recordLogEvent($event) { $this->eventBus->log($this->getRequest()->getIP(), $event); $this->logAction($event); } /** * @todo Hook up $httpCode * @param wfWAFBlockException $e * @param int $httpCode */ public function blockAction($e, $httpCode = 403, $redirect = false, $template = null) { $this->getStorageEngine()->logAttack($e->getFailedRules(), $e->getParamKey(), $e->getParamValue(), $e->getRequest(), $e->getRequest()->getMetadata()); if ($redirect) { wfWAFUtils::redirect($redirect); // exits and emits no cache headers } if ($httpCode == 503) { wfWAFUtils::statusHeader(503); wfWAFUtils::doNotCache(); if ($secsToGo = $e->getRequest()->getMetadata('503Time')) { header('Retry-After: ' . $secsToGo); } exit($this->getUnavailableMessage($e->getRequest()->getMetadata('503Reason'), $template)); } header('HTTP/1.0 403 Forbidden'); wfWAFUtils::doNotCache(); exit($this->getBlockedMessage($template)); } /** * @todo Hook up $httpCode * @param wfWAFBlockXSSException $e * @param int $httpCode */ public function blockXSSAction($e, $httpCode = 403, $redirect = false) { $this->getStorageEngine()->logAttack($e->getFailedRules(), $e->getParamKey(), $e->getParamValue(), $e->getRequest(), $e->getRequest()->getMetadata()); if ($redirect) { wfWAFUtils::redirect($redirect); // exits and emits no cache headers } if ($httpCode == 503) { wfWAFUtils::statusHeader(503); wfWAFUtils::doNotCache(); if ($secsToGo = $e->getRequest()->getMetadata('503Time')) { header('Retry-After: ' . $secsToGo); } exit($this->getUnavailableMessage($e->getRequest()->getMetadata('503Reason'))); } header('HTTP/1.0 403 Forbidden'); wfWAFUtils::doNotCache(); exit($this->getBlockedMessage()); } public function logAction($event) { $failedRules = array_merge(array('logged'), $event->getFailedRules()); $this->getStorageEngine()->logAttack($failedRules, $event->getParamKey(), $event->getParamValue(), $this->getRequest()); } /** * @return string */ public function getBlockedMessage($template = null) { if ($template === null) { if ($this->currentUserCanWhitelist()) { $template = '403-roadblock'; } else { $template = '403'; } } try { $homeURL = wfWAF::getInstance()->getStorageEngine()->getConfig('homeURL', null, 'synced'); $siteURL = wfWAF::getInstance()->getStorageEngine()->getConfig('siteURL', null, 'synced'); $customText = wfWAF::getInstance()->getStorageEngine()->getConfig('blockCustomText', null, 'synced'); $errorNonce = ''; if ($authCookie = wfWAF::getInstance()->parseAuthCookie()) { $errorNonce = wfWAF::getInstance()->getStorageEngine()->getConfig('errorNonce_' . (int) $authCookie['userID'], '', 'synced'); } } catch (Exception $e) { //Do nothing } return wfWAFView::create($template, array( 'waf' => $this, 'homeURL' => $homeURL, 'siteURL' => $siteURL, 'customText' => $customText, 'errorNonce' => $errorNonce, ))->render(); } /** * @return string */ public function getUnavailableMessage($reason = '', $template = null) { if ($template === null) { $template = '503'; } try { $homeURL = wfWAF::getInstance()->getStorageEngine()->getConfig('homeURL', null, 'synced'); $siteURL = wfWAF::getInstance()->getStorageEngine()->getConfig('siteURL', null, 'synced'); $customText = wfWAF::getInstance()->getStorageEngine()->getConfig('blockCustomText', null, 'synced'); $errorNonce = ''; if ($authCookie = wfWAF::getInstance()->parseAuthCookie()) { $errorNonce = wfWAF::getInstance()->getStorageEngine()->getConfig('errorNonce_' . (int) $authCookie['userID'], '', 'synced'); } } catch (Exception $e) { //Do nothing } return wfWAFView::create($template, array( 'waf' => $this, 'reason' => $reason, 'homeURL' => $homeURL, 'siteURL' => $siteURL, 'customText' => $customText, 'errorNonce' => $errorNonce, ))->render(); } /** * */ public function whitelistFailedRules() { foreach ($this->failedRules as $paramKey => $categories) { foreach ($categories as $category => $failedRules) { foreach ($failedRules as $failedRule) { /** * @var wfWAFRule $rule * @var wfWAFRuleComparisonFailure $failedComparison */ $rule = $failedRule['rule']; if ($rule->getWhitelist()) { $failedComparison = $failedRule['failedComparison']; $data = array( 'timestamp' => time(), 'description' => 'Allowlisted while in Learning Mode.', 'source' => 'learning-mode', 'ip' => $this->getRequest()->getIP(), ); if (function_exists('get_current_user_id')) { $data['userID'] = get_current_user_id(); } $this->whitelistRuleForParam($this->getRequest()->getPath(), $failedComparison->getParamKey(), $rule->getRuleID(), $data); } } } } } /** * @param string $path * @param string $paramKey * @param int $ruleID * @param array $data */ public function whitelistRuleForParam($path, $paramKey, $ruleID, $data = array()) { if ($this->isParamKeyURLBlacklisted($ruleID, $paramKey, $path)) { return; } $whitelist = $this->getStorageEngine()->getConfig('whitelistedURLParams', null, 'livewaf'); if (is_object($whitelist)) { $whitelist = (array) $whitelist; } else if (!is_array($whitelist)) { $whitelist = array(); } if (is_array($ruleID)) { foreach ($ruleID as $id) { $whitelist[base64_encode($path) . "|" . base64_encode($paramKey)][$id] = $data; } } else { $whitelist[base64_encode($path) . "|" . base64_encode($paramKey)][$ruleID] = $data; } $this->getStorageEngine()->setConfig('whitelistedURLParams', $whitelist, 'livewaf'); } /** * @param int $ruleID * @param string $urlPath * @param string $paramKey * @return bool */ public function isRuleParamWhitelisted($ruleID, $urlPath, $paramKey) { $urlPath = (string)$urlPath; $paramKey = (string)$paramKey; if ($this->isParamKeyURLBlacklisted($ruleID, $paramKey, $urlPath)) { return false; } if ($paramKey==='none' || (is_array($this->whitelistedParams) && array_key_exists($paramKey, $this->whitelistedParams) && is_array($this->whitelistedParams[$paramKey])) ) { foreach ($this->whitelistedParams[$paramKey] as $urlRegex) { if (is_array($urlRegex) || $urlRegex instanceof wfWAFRuleParserURLParam) { $rules = is_array($urlRegex) ? $urlRegex['rules'] : $urlRegex->getRules(); if ($rules && !in_array($ruleID, $rules)) { continue; } $conditional = is_array($urlRegex) ? (isset($urlRegex['conditional']) ? $urlRegex['conditional'] : null) : $urlRegex->getConditional(); if ($conditional && !$conditional->evaluate()) { continue; } $urlRegex = is_array($urlRegex) ? $urlRegex['url'] : $urlRegex->getUrl(); } if (is_string($urlRegex) && preg_match($urlRegex, $urlPath)) { return true; } } } $whitelistKey = base64_encode($urlPath) . "|" . base64_encode($paramKey); $whitelist = $this->getStorageEngine()->getConfig('whitelistedURLParams', array(), 'livewaf'); if (is_object($whitelist)) { $whitelist = (array) $whitelist; } else if (!is_array($whitelist)) { $whitelist = array(); } if (array_key_exists($whitelistKey, $whitelist)) { foreach (array('all', $ruleID) as $key) { if (array_key_exists($key, $whitelist[$whitelistKey])) { $ruleData = $whitelist[$whitelistKey][$key]; if (is_array($ruleData) && array_key_exists('disabled', $ruleData)) { return !$ruleData['disabled']; } else if ($ruleData) { return true; } } } } return false; } /** * */ public function sendAttackData() { if ($this->getStorageEngine()->getConfig('attackDataKey', false) === false) { $this->getStorageEngine()->setConfig('attackDataKey', mt_rand(0, 0xfff)); } if (!$this->getStorageEngine()->getConfig('other_WFNet', true, 'synced')) { $this->getStorageEngine()->truncateAttackData(); $this->getStorageEngine()->unsetConfig('attackDataNextInterval', 'transient'); return; } $request = new wfWAFHTTP(); try { $response = wfWAFHTTP::get( sprintf(WFWAF_API_URL_SEC . "waf-rules/%d.txt", $this->getStorageEngine()->getConfig('attackDataKey')), $request); if ($response instanceof wfWAFHTTPResponse) { if ($response->getBody() === 'ok') { $request = new wfWAFHTTP(); $request->setHeaders(array( 'Content-Type' => 'application/json', )); $response = wfWAFHTTP::post(WFWAF_API_URL_SEC . "?" . http_build_query(array( 'action' => 'send_waf_attack_data', 'k' => $this->getStorageEngine()->getConfig('apiKey', null, 'synced'), 's' => $this->getStorageEngine()->getConfig('siteURL', null, 'synced') ? $this->getStorageEngine()->getConfig('siteURL', null, 'synced') : sprintf('%s://%s/', $this->getRequest()->getProtocol(), rawurlencode($this->getRequest()->getHost())), 'h' => $this->getStorageEngine()->getConfig('homeURL', null, 'synced') ? $this->getStorageEngine()->getConfig('homeURL', null, 'synced') : sprintf('%s://%s/', $this->getRequest()->getProtocol(), rawurlencode($this->getRequest()->getHost())), 't' => microtime(true), 'lang' => $this->getStorageEngine()->getConfig('WPLANG', null, 'synced'), ), '', '&'), $this->getStorageEngine()->getAttackData(), $request); if ($response instanceof wfWAFHTTPResponse && $response->getBody()) { $jsonData = wfWAFUtils::json_decode($response->getBody(), true); if (is_array($jsonData) && array_key_exists('success', $jsonData)) { $this->getStorageEngine()->truncateAttackData(); $this->getStorageEngine()->unsetConfig('attackDataNextInterval', 'transient'); } } } else if (is_string($response->getBody()) && preg_match('/next check in: ([0-9]+)/', $response->getBody(), $matches)) { $this->getStorageEngine()->setConfig('attackDataNextInterval', time() + $matches[1], 'transient'); if ($this->getStorageEngine()->isAttackDataFull()) { $this->getStorageEngine()->truncateAttackData(); } } // Could be that the server is down, so hold off on sending data for a little while. } else { $this->getStorageEngine()->setConfig('attackDataNextInterval', time() + 7200, 'transient'); } } catch (wfWAFHTTPTransportException $e) { error_log($e->getMessage()); } } /** * @param string $action * @return array */ public function isAllowedAction($action) { static $actions; if (!isset($actions)) { $actions = array_flip($this->getAllowedActions()); } return array_key_exists($action, $actions); } /** * @return array */ public function getAllowedActions() { return array('fail', 'allow', 'block', 'failXSS', 'blockXSS', 'failSQLi', 'blockSQLi', 'log'); } /** * */ public function uninstall() { $this->getStorageEngine()->uninstall(); } public function fileList() { $fileList = array(); $rulesFile = $this->getCompiledRulesFile(); if ($rulesFile !== null) array_push($fileList, $rulesFile); if (method_exists($this->getStorageEngine(), 'fileList')) { $fileList = array_merge($fileList, $this->getStorageEngine()->fileList()); } return $fileList; } /** * @param int $ruleID * @param string $paramKey * @param string $urlPath * @return bool */ public function isParamKeyURLBlacklisted($ruleID, $paramKey, $urlPath) { if (is_array($this->blacklistedParams) && array_key_exists($paramKey, $this->blacklistedParams) && is_array($this->blacklistedParams[$paramKey]) ) { foreach ($this->blacklistedParams[$paramKey] as $urlRegex) { if (is_array($urlRegex) || $urlRegex instanceof wfWAFRuleParserURLParam) { $rules = is_array($urlRegex) ? $urlRegex['rules'] : $urlRegex->getRules(); if ($rules && !in_array($ruleID, $rules)) { continue; } $conditional = is_array($urlRegex) ? (isset($urlRegex['conditional']) ? $urlRegex['conditional'] : null) : $urlRegex->getConditional(); if ($conditional && !$conditional->evaluate()) { continue; } $urlRegex = is_array($urlRegex) ? $urlRegex['url'] : $urlRegex->getUrl(); } if (is_string($urlRegex) && preg_match($urlRegex, $urlPath)) { return true; } } } return false; } /** * @return bool */ public function currentUserCanWhitelist() { if ($authCookie = $this->parseAuthCookie()) { return $authCookie['role'] === 'administrator'; } return false; } /** * @param string $capability * @return bool */ public function checkCapability($capability) { if ($authCookie = $this->parseAuthCookie()) { return $authCookie['capabilities']!==null && in_array($capability, $authCookie['capabilities']); } return false; } /** * @param string|null $cookieVal * @return bool */ public function parseAuthCookie($cookieVal = null) { if ($cookieVal === null) { $cookieName = $this->getAuthCookieName(); $cookieVal = !empty($_COOKIE[$cookieName]) && is_string($_COOKIE[$cookieName]) ? $_COOKIE[$cookieName] : ''; } $pieces = explode('|', $cookieVal); $pieceCount = count($pieces); if ($pieceCount === 4) { list($userID, $role, $capabilityList, $signature) = $pieces; $capabilities = empty($capabilityList) ? array() : explode(',', $capabilityList); } else if ($pieceCount === 3) { list($userID, $role, $signature) = $pieces; $capabilities = null; } else { return false; } if (wfWAFUtils::hash_equals($signature, $this->getAuthCookieValue($userID, $role, $capabilities))) { return array( 'userID' => $userID, 'role' => $role, 'capabilities' => $capabilities ); } return false; } /** * @param int|string $userID * @param string $role * @param array $capabilities * @return bool|string */ public function getAuthCookieValue($userID, $role, $capabilities = array()) { if (!is_array($capabilities)) $capabilities = array(); $algo = function_exists('hash') ? 'sha256' : 'sha1'; return wfWAFUtils::hash_hmac($algo, $userID . $role . '|'. implode(',', $capabilities) . floor(time() / 43200), $this->getStorageEngine()->getConfig('authKey')); } /** * @param string $action * @return bool|string */ public function createNonce($action) { $userInfo = $this->parseAuthCookie(); if ($userInfo === false) { $userInfo = array('userID' => 0, 'role' => ''); // Use an empty user like WordPress would } $userID = $userInfo['userID']; $role = $userInfo['role']; $algo = function_exists('hash') ? 'sha256' : 'sha1'; return wfWAFUtils::hash_hmac($algo, $action . $userID . $role . floor(time() / 43200), $this->getStorageEngine()->getConfig('authKey')); } /** * @param string $nonce * @param string $action * @return bool */ public function verifyNonce($nonce, $action) { if (empty($nonce)) { return false; } return wfWAFUtils::hash_equals($nonce, $this->createNonce($action)); } /** * @param string|null $host * @return string */ public function getAuthCookieName($host = null) { if ($host === null) { $host = $this->getRequest()->getHost(); } return self::AUTH_COOKIE . '-' . md5((string)$host); } /** * @return string */ public function getCompiledRulesFile() { return $this->rulesFile; } /** * @param string $rulesFile */ public function setCompiledRulesFile($rulesFile) { $this->rulesFile = $rulesFile; } /** * @param $ip * @return mixed */ public function isIPBlocked($ip) { return $this->getStorageEngine()->isIPBlocked($ip); } /** * @param wfWAFRequest $request * @return bool|array false if it should not be blocked, otherwise an array defining the context for the final action */ public function willPerformFinalAction($request) { return false; } /** * @return array */ public function getTrippedRules() { return $this->trippedRules; } /** * @return array */ public function getTrippedRuleIDs() { $ret = array(); /** @var wfWAFRule $rule */ foreach ($this->getTrippedRules() as $rule) { $ret[] = $rule->getRuleID(); } return $ret; } public function showBench() { return sprintf("Bench: %f seconds\n\n", microtime(true) - $this->getRequest()->getTimestamp()); } public function debug() { return join("\n", $this->debug) . "\n\n" . $this->showBench(); // $debug = ''; // /** @var wfWAFRule $rule */ // foreach ($this->trippedRules as $rule) { // $debug .= $rule->debug(); // } // return $debug; } /** * @return array */ public function getScores() { return $this->scores; } /** * @param string $var * @return null */ public function getVariable($var) { if (array_key_exists($var, $this->variables)) { return $this->variables[$var]; } return null; } /** * @return wfWAFRequestInterface */ public function getRequest() { return $this->request; } /** * @param wfWAFRequestInterface $request */ public function setRequest($request) { $this->request = $request; } /** * @return wfWAFStorageInterface */ public function getStorageEngine() { return $this->storageEngine; } /** * @param wfWAFStorageInterface $storageEngine */ public function setStorageEngine($storageEngine) { $this->storageEngine = $storageEngine; } /** * @return wfWAFEventBus */ public function getEventBus() { return $this->eventBus; } /** * @param wfWAFEventBus $eventBus */ public function setEventBus($eventBus) { $this->eventBus = $eventBus; } /** * @return array */ public function getRules() { return $this->rules; } /** * @param array $rules */ public function setRules($rules) { $this->rules = $rules; } /** * @param int $ruleID * @return null|wfWAFRule */ public function getRule($ruleID) { $rules = $this->getRules(); if (is_array($rules) && array_key_exists($ruleID, $rules)) { return $rules[$ruleID]; } return null; } /** * @return string */ public function getPublicKey() { return $this->publicKey; } /** * @param string $publicKey */ public function setPublicKey($publicKey) { $this->publicKey = $publicKey; } /** * @return array */ public function getFailedRules() { return $this->failedRules; } public function getCookieRedactionPatterns($retry = true) { $patterns = $this->getStorageEngine()->getConfig('cookieRedactionPatterns', null, 'transient'); if ($patterns === null) { if ($retry) { $event = new wfWAFCronFetchCookieRedactionPatternsEvent(time()); $event->setWaf($this); $event->fire(); return $this->getCookieRedactionPatterns(false); } } else { $patterns = wfWAFUtils::json_decode($patterns, true); if (is_array($patterns)) return $patterns; } return null; } public function getVersion() { return WFWAF_VERSION; } } require_once __DIR__ . '/api.php'; /** * Serialized for use with the WAF cron. */ abstract class wfWAFCronEvent { abstract public function fire(); abstract public function getNextFireTime(); protected $fireTime; private $waf; /** * @param int $fireTime */ public function __construct($fireTime = null) { $this->setFireTime($fireTime === null ? $this->getNextFireTime() : $fireTime); } /** * @param int|null $time * @return bool */ public function isInPast($time = null) { if ($time === null) { $time = time(); } return $this->getFireTime() <= $time; } public function __sleep() { return array('fireTime'); } /** * @return mixed */ public function getFireTime() { return $this->fireTime; } /** * @param mixed $fireTime */ public function setFireTime($fireTime) { $this->fireTime = $fireTime; } /** * @return wfWAF */ public function getWaf() { return $this->waf; } /** * @param wfWAF $waf */ public function setWaf($waf) { $this->waf = $waf; } public function reschedule() { $nextFireTime = $this->getNextFireTime(); if ($nextFireTime === null) return false; $newEvent = new static($nextFireTime); return $newEvent; } } class wfWAFCronFetchRulesEvent extends wfWAFCronEvent { /** * @var wfWAFHTTPResponse */ private $response; private $forceUpdate; public function __construct($fireTime, $forceUpdate = false) { parent::__construct($fireTime); $this->forceUpdate = $forceUpdate; } public function fire() { $waf = $this->getWaf(); if (!$waf) { return false; } $success = true; $guessSiteURL = sprintf('%s://%s/', $waf->getRequest()->getProtocol(), $waf->getRequest()->getHost()); try { $payload = array( 'action' => 'get_waf_rules', 'k' => $waf->getStorageEngine()->getConfig('apiKey', null, 'synced'), 's' => $waf->getStorageEngine()->getConfig('siteURL', null, 'synced') ? $waf->getStorageEngine()->getConfig('siteURL', null, 'synced') : $guessSiteURL, 'h' => $waf->getStorageEngine()->getConfig('homeURL', null, 'synced') ? $waf->getStorageEngine()->getConfig('homeURL', null, 'synced') : $guessSiteURL, 'openssl' => $waf->hasOpenSSL() ? 1 : 0, 'lang' => $waf->getStorageEngine()->getConfig('WPLANG', null, 'synced'), 'waf_version' => $waf->getVersion() ); $lastRuleHash=$this->forceUpdate ? null : $waf->getStorageEngine()->getConfig('lastRuleHash', null, 'transient'); if($lastRuleHash!==null) $payload['hash']=$lastRuleHash; if ($waf->getStorageEngine()->getConfig('other_WFNet', true, 'synced')) { $payload['disabled'] = implode('|', $waf->getDisabledRuleIDs()); } $this->response = wfWAFHTTP::get(WFWAF_API_URL_SEC . "?" . http_build_query($payload, '', '&'), null, 10, 5); if ($this->response) { if($this->response->getStatusCode() !== 304){ $jsonData = wfWAFUtils::json_decode($this->response->getBody(), true); if (is_array($jsonData)) { if ($waf->hasOpenSSL() && isset($jsonData['data']['signature256']) && isset($jsonData['data']['rules']) && $waf->verifySignedRequest(base64_decode($jsonData['data']['signature256']), $jsonData['data']['rules']) ) { $waf->updateRuleSet(base64_decode($jsonData['data']['rules']), isset($jsonData['data']['timestamp']) ? $jsonData['data']['timestamp'] : true); $waf->getStorageEngine()->setConfig('lastRuleHash', $jsonData['data']['signature256'], 'transient'); if (array_key_exists('premiumCount', $jsonData['data'])) { $waf->getStorageEngine()->setConfig('premiumCount', $jsonData['data']['premiumCount'], 'transient'); } } else if (!$waf->hasOpenSSL() && isset($jsonData['data']['hash']) && isset($jsonData['data']['rules']) && $waf->verifyHashedRequest($jsonData['data']['hash'], $jsonData['data']['rules']) ) { $waf->updateRuleSet(base64_decode($jsonData['data']['rules']), isset($jsonData['data']['timestamp']) ? $jsonData['data']['timestamp'] : true); $waf->getStorageEngine()->setConfig('lastRuleHash', $jsonData['data']['hash'], 'transient'); if (array_key_exists('premiumCount', $jsonData['data'])) { $waf->getStorageEngine()->setConfig('premiumCount', $jsonData['data']['premiumCount'], 'transient'); } } else { $success = false; } } else { $success = false; } } } else { $success = false; } $lastMalwareSignatureUpdate=$waf->getStorageEngine()->getConfig('signaturesLastUpdated', 0, 'transient'); $isPaid=$waf->getStorageEngine()->getConfig('isPaid', false, 'synced'); //Only update malware signatures for free sites if they are older than 3 days plus an hour if ($isPaid || $this->forceUpdate || $lastMalwareSignatureUpdate < (time() - (259200 + 3600))) { $this->response = wfWAFHTTP::get(WFWAF_API_URL_SEC . "?" . http_build_query(array( 'action' => 'get_malware_signatures', 'k' => $waf->getStorageEngine()->getConfig('apiKey', null, 'synced'), 's' => $waf->getStorageEngine()->getConfig('siteURL', null, 'synced') ? $waf->getStorageEngine()->getConfig('siteURL', null, 'synced') : $guessSiteURL, 'h' => $waf->getStorageEngine()->getConfig('homeURL', null, 'synced') ? $waf->getStorageEngine()->getConfig('homeURL', null, 'synced') : $guessSiteURL, 'openssl' => $waf->hasOpenSSL() ? 1 : 0, 'hash' => $this->forceUpdate ? null : $waf->getStorageEngine()->getConfig('lastMalwareHash', null, 'transient'), 'cs-hash' => $this->forceUpdate ? null : $waf->getStorageEngine()->getConfig('lastMalwareHashCommonStrings', null, 'transient'), 'lang' => $waf->getStorageEngine()->getConfig('WPLANG', null, 'synced') ), '', '&'), null, 15, 5); if ($this->response) { if($this->response->getStatusCode() !== 304){ $jsonData = wfWAFUtils::json_decode($this->response->getBody(), true); if (is_array($jsonData)) { if ($waf->hasOpenSSL() && isset($jsonData['data']['signature256']) && isset($jsonData['data']['signatures']) && $waf->verifySignedRequest(base64_decode($jsonData['data']['signature256']), $jsonData['data']['signatures']) ) { $waf->setMalwareSignatures(wfWAFUtils::json_decode(base64_decode($jsonData['data']['signatures'])), isset($jsonData['data']['timestamp']) ? $jsonData['data']['timestamp'] : true); $waf->getStorageEngine()->setConfig('lastMalwareHash', $jsonData['data']['signature256'], 'transient'); if (array_key_exists('premiumCount', $jsonData['data'])) { $waf->getStorageEngine()->setConfig('signaturePremiumCount', $jsonData['data']['premiumCount'], 'transient'); } if (array_key_exists('commonStringsSignature256', $jsonData['data']) && array_key_exists('commonStrings', $jsonData['data']) && array_key_exists('signatureIndexes', $jsonData['data']) && $waf->verifySignedRequest(base64_decode($jsonData['data']['commonStringsSignature256']), $jsonData['data']['commonStrings'] . $jsonData['data']['signatureIndexes']) ) { $waf->setMalwareSignatureCommonStrings(wfWAFUtils::json_decode(base64_decode($jsonData['data']['commonStrings'])), wfWAFUtils::json_decode(base64_decode($jsonData['data']['signatureIndexes']))); $waf->getStorageEngine()->setConfig('lastMalwareHashCommonStrings', $jsonData['data']['commonStringsSignature256'], 'transient'); } } else if (!$waf->hasOpenSSL() && isset($jsonData['data']['hash']) && isset($jsonData['data']['signatures']) && $waf->verifyHashedRequest($jsonData['data']['hash'], $jsonData['data']['signatures']) ) { $waf->setMalwareSignatures(wfWAFUtils::json_decode(base64_decode($jsonData['data']['signatures'])), isset($jsonData['data']['timestamp']) ? $jsonData['data']['timestamp'] : true); $waf->getStorageEngine()->setConfig('lastMalwareHash', $jsonData['data']['hash'], 'transient'); if (array_key_exists('premiumCount', $jsonData['data'])) { $waf->getStorageEngine()->setConfig('signaturePremiumCount', $jsonData['data']['premiumCount'], 'transient'); } if (array_key_exists('commonStringsHash', $jsonData['data']) && array_key_exists('commonStrings', $jsonData['data']) && array_key_exists('signatureIndexes', $jsonData['data']) && $waf->verifyHashedRequest($jsonData['data']['commonStringsHash'], $jsonData['data']['commonStrings'] . $jsonData['data']['signatureIndexes']) ) { $waf->setMalwareSignatureCommonStrings(wfWAFUtils::json_decode(base64_decode($jsonData['data']['commonStrings'])), wfWAFUtils::json_decode(base64_decode($jsonData['data']['signatureIndexes']))); $waf->getStorageEngine()->setConfig('lastMalwareHashCommonStrings', $jsonData['data']['commonStringsHash'], 'transient'); } } else { $success = false; } } else { $success = false; } } } else { $success = false; } } } catch (wfWAFHTTPTransportException $e) { error_log($e->getMessage()); $success = false; } catch (wfWAFBuildRulesException $e) { error_log($e->getMessage()); $success = false; } if ($success) { $waf->getStorageEngine()->setConfig('lastRuleUpdateCheck', time(), 'transient'); } return $success; } public function getNextFireTime() { $waf = $this->getWaf(); if (!$waf) return null; if ($this->response) { $headers = $this->response->getHeaders(); if (isset($headers['Expires'])) { $timestamp = strtotime($headers['Expires']); // Make sure it's at least 2 hours ahead. if ($timestamp && $timestamp > (time() + 7200)) { return $timestamp; } } } return time() + (86400 * ($waf->getStorageEngine()->getConfig('isPaid', null, 'synced') ? .5 : 7)); } public function getResponse() { return $this->response; } } class wfWAFCronFetchIPListEvent extends wfWAFCronEvent { public function fire() { //No op -- removed but class retained in case it still exists in serialized cron data } public function getNextFireTime() { return null; } } class wfWAFCronFetchBlacklistPrefixesEvent extends wfWAFCronEvent { public function fire() { $waf = $this->getWaf(); if (!$waf) { return; } $guessSiteURL = sprintf('%s://%s/', $waf->getRequest()->getProtocol(), $waf->getRequest()->getHost()); try { if ($waf->getStorageEngine()->getConfig('isPaid', null, 'synced')) { $request = new wfWAFHTTP(); $response = wfWAFHTTP::get(WFWAF_API_URL_SEC . 'blacklist-prefixes.bin' . "?" . http_build_query(array( 'k' => $waf->getStorageEngine()->getConfig('apiKey', null, 'synced'), 's' => $waf->getStorageEngine()->getConfig('siteURL', null, 'synced') ? $waf->getStorageEngine()->getConfig('siteURL', null, 'synced') : $guessSiteURL, 'h' => $waf->getStorageEngine()->getConfig('homeURL', null, 'synced') ? $waf->getStorageEngine()->getConfig('homeURL', null, 'synced') : $guessSiteURL, 't' => microtime(true), 'lang' => $waf->getStorageEngine()->getConfig('WPLANG', null, 'synced'), ), '', '&'), $request); if ($response instanceof wfWAFHTTPResponse && $response->getBody()) { $waf->getStorageEngine()->setConfig('blockedPrefixes', base64_encode($response->getBody()), 'transient'); $waf->getStorageEngine()->setConfig('blacklistAllowedCache', '', 'transient'); } } $waf->getStorageEngine()->vacuum(); } catch (wfWAFHTTPTransportException $e) { error_log($e->getMessage()); } } public function getNextFireTime() { return time() + 7200; } } class wfWAFCronFetchCookieRedactionPatternsEvent extends wfWAFCronEvent { const INTERVAL = 604800; const RETRY_DELAY = 14400; public function fire() { $waf = $this->getWaf(); if (!$waf) return; $storageEngine = $waf->getStorageEngine(); $lastFailure = $storageEngine->getConfig('cookieRedactionLastUpdateFailure', null, 'transient'); if ($lastFailure !== null && time() - (int) $lastFailure < self::RETRY_DELAY) return; try { $api = new wfWafApi($waf); $response = $api->actionGet('get_cookie_redaction_patterns'); if ($response->getStatusCode() === 200) { $body = $response->getBody(); $data = wfWAFUtils::json_decode($body, true); if (is_array($data) && array_key_exists('data', $data)) { $patterns = $data['data']; if (is_array($patterns)) { $storageEngine->setConfig('cookieRedactionPatterns', wfWAFUtils::json_encode($patterns), 'transient'); return; } } error_log('Malformed cookie redaction patterns received, response body: ' . print_r($body, true)); } else { error_log('Failed to retrieve cookie redaction patterns, response code: ' . $response->getStatusCode()); } } catch (wfWafMissingApiKeyException $e) { // This is intentionally ignored as the API key may be missing during initial setup of the plugin } catch (wfWafApiException $e) { error_log('Failed to retrieve cookie redaction patterns: ' . $e->getMessage()); } $storageEngine->setConfig('cookieRedactionLastUpdateFailure', time(), 'transient'); } public function getNextFireTime() { return time() + self::INTERVAL; } } interface wfWAFObserver { public function prevBlocked($ip); public function block($ip, $exception); public function allow($ip, $exception); public function blockXSS($ip, $exception); public function blockSQLi($ip, $exception); public function log($ip, $event); public function wafDisabled(); public function beforeRunRules(); public function afterRunRules(); } class wfWAFEventBus implements wfWAFObserver { private $observers = array(); /** * @param wfWAFObserver $observer * @throws wfWAFEventBusException */ public function attach($observer) { if (!($observer instanceof wfWAFObserver)) { throw new wfWAFEventBusException('Observer supplied to wfWAFEventBus::attach must implement wfWAFObserver'); } $this->observers[] = $observer; } /** * @param wfWAFObserver $observer */ public function detach($observer) { $key = array_search($observer, $this->observers, true); if ($key !== false) { unset($this->observers[$key]); } } public function prevBlocked($ip) { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->prevBlocked($ip); } } public function block($ip, $exception) { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->block($ip, $exception); } } public function allow($ip, $exception) { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->allow($ip, $exception); } } public function blockXSS($ip, $exception) { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->blockXSS($ip, $exception); } } public function blockSQLi($ip, $exception) { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->blockSQLi($ip, $exception); } } public function log($ip, $event) { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->log($ip, $event); } } public function wafDisabled() { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->wafDisabled(); } } public function beforeRunRules() { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->beforeRunRules(); } } public function afterRunRules() { /** @var wfWAFObserver $observer */ foreach ($this->observers as $observer) { $observer->afterRunRules(); } } } class wfWAFBaseObserver implements wfWAFObserver { public function prevBlocked($ip) { } public function block($ip, $exception) { } public function allow($ip, $exception) { } public function blockXSS($ip, $exception) { } public function blockSQLi($ip, $exception) { } public function log($ip, $exception) { } public function wafDisabled() { } public function beforeRunRules() { } public function afterRunRules() { } } class wfWAFException extends Exception { } class wfWAFRunException extends Exception { /** @var array */ private $failedRules; /** @var string */ private $paramKey; /** @var string */ private $paramValue; /** @var wfWAFRequestInterface */ private $request; /** * @return array */ public function getFailedRules() { return $this->failedRules; } /** * @param array $failedRules */ public function setFailedRules($failedRules) { $this->failedRules = $failedRules; } /** * @return string */ public function getParamKey() { return $this->paramKey; } /** * @param string $paramKey */ public function setParamKey($paramKey) { $this->paramKey = $paramKey; } /** * @return string */ public function getParamValue() { return $this->paramValue; } /** * @param string $paramValue */ public function setParamValue($paramValue) { $this->paramValue = $paramValue; } /** * @return wfWAFRequestInterface */ public function getRequest() { return $this->request; } /** * @param wfWAFRequestInterface $request */ public function setRequest($request) { $this->request = $request; } } class wfWAFAllowException extends wfWAFRunException { } class wfWAFBlockException extends wfWAFRunException { } class wfWAFBlockXSSException extends wfWAFRunException { } class wfWAFBlockSQLiException extends wfWAFRunException { } class wfWAFBuildRulesException extends wfWAFException { } class wfWAFEventBusException extends wfWAFException { } } class wfWAFLogEvent { private $failedRules; private $paramKey, $paramValue; private $request; public function __construct($failedRules=array(), $paramKey=null, $paramValue=null, $request=null){ $this->failedRules=$failedRules; $this->paramKey=$paramKey; $this->paramValue=$paramValue; $this->request=$request; } public function getFailedRules(){ return $this->failedRules; } public function getParamKey(){ return $this->paramKey; } public function getParamValue(){ return $this->paramValue; } public function getRequest(){ return $this->request; } }\n"; const LOG_INFO_HEADER = "******************************************************************\nThis file is used by the Wordfence Web Application Firewall. Read \nmore at https://docs.wordfence.com/en/Web_Application_Firewall_FAQ\n******************************************************************\n"; const IP_BLOCK_RECORD_SIZE = 24; private $rules; private $failScores; private $variables; private $whitelistedParams; private $blacklistedParams; public static function allowFileWriting() { if (defined('WFWAF_ALWAYS_ALLOW_FILE_WRITING') && WFWAF_ALWAYS_ALLOW_FILE_WRITING) { return true; } if (wfWAFUtils::isCli()) { return false; } return true; } public static function atomicFilePutContents($file, $content, $prefix = 'config') { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $tmpFile = @tempnam(dirname($file), $prefix . '.tmp.'); if (!$tmpFile) { $tmpFile = @tempnam(sys_get_temp_dir(), $prefix . '.tmp.'); } if (!$tmpFile) { throw new wfWAFStorageFileException('Unable to save temporary file for atomic writing.'); } $tmpHandle = @fopen($tmpFile, 'w'); if (!$tmpHandle) { throw new wfWAFStorageFileException('Unable to save temporary file ' . $tmpFile . ' for atomic writing.'); } self::lock($tmpHandle, LOCK_EX); fwrite($tmpHandle, $content); fflush($tmpHandle); self::lock($tmpHandle, LOCK_UN); fclose($tmpHandle); chmod($tmpFile, self::permissions()); // Attempt to verify file has finished writing (sometimes the disk will lie for better benchmarks) $tmpContents = file_get_contents($tmpFile); if ($tmpContents !== $content) { throw new wfWAFStorageFileException('Unable to verify temporary file contents for atomic writing.'); } if (!@rename($tmpFile, $file)) { $backFile = @tempnam(dirname($file), $prefix . '.bak.'); if (!$backFile) { $backFile = @tempnam(sys_get_temp_dir(), $prefix . '.bak.'); } if (!$backFile) { throw new wfWAFStorageFileException('Unable to save temporary file for atomic writing.'); } if (WFWAF_DEBUG) { rename($file, $backFile); rename($tmpFile, $file); unlink($backFile); unlink($tmpFile); } else { @rename($file, $backFile); @rename($tmpFile, $file); @unlink($backFile); @unlink($tmpFile); } } } public static function lock($handle, $lock, $wouldLock = 1) { $locked = flock($handle, $lock, $wouldLock); if (!$locked) { error_log('Lock not acquired ' . $locked); } return $locked; } public static function permissions() { if (defined('WFWAF_LOG_FILE_MODE')) { return WFWAF_LOG_FILE_MODE; } static $_cachedPermissions = null; if ($_cachedPermissions === null) { if (defined('WFWAF_LOG_PATH')) { $template = rtrim(WFWAF_LOG_PATH, '/') . '/template.php'; if (file_exists($template)) { $stat = @stat($template); if ($stat !== false) { $mode = $stat[2]; $updatedMode = 0600; if (($mode & 0020) == 0020) { $updatedMode = $updatedMode | 0060; } $_cachedPermissions = $updatedMode; return $updatedMode; } } } return 0660; } return $_cachedPermissions; } /** * @var resource */ private $ipCacheFileHandle; /** * @var string|null */ private $attackDataFile; /** * @var wfWAFAttackDataStorageFileEngine */ private $attackDataEngine; /** * @var string|null */ private $ipCacheFile; private $configFile; private $rulesFile; private $rulesDSLCacheFile; private $dataChanged = array(); private $data = array(); /** * @var resource[] */ private $configFileHandles = array(); private $uninstalled; private $attackDataRows; private $attackDataNewerThan; /** * @param string|null $attackDataFile * @param string|null $ipCacheFile * @param string|null $configFile * @param string|null $rulesFile * @param null $rulesDSLCacheFile */ public function __construct($attackDataFile = null, $ipCacheFile = null, $configFile = null, $rulesFile = null, $rulesDSLCacheFile = null) { $this->setAttackDataFile($attackDataFile); $this->setIPCacheFile($ipCacheFile); $this->setConfigFile($configFile); $this->setRulesFile($rulesFile); $this->setRulesDSLCacheFile($rulesDSLCacheFile); } /** * @param float $olderThan * @return bool * @throws wfWAFStorageFileException */ public function hasPreviousAttackData($olderThan) { $this->open(); $timestamp = $this->getAttackDataEngine()->getOldestTimestamp(); return $timestamp && $timestamp < $olderThan; } /** * @param float $newerThan * @return bool * @throws wfWAFStorageFileException */ public function hasNewerAttackData($newerThan) { $this->open(); $timestamp = $this->getAttackDataEngine()->getNewestTimestamp(); return $timestamp && $timestamp > $newerThan; } /** * @return mixed|string|void * @throws wfWAFStorageFileException */ public function getAttackData() { $this->open(); $this->attackDataRows = array(); $this->getAttackDataEngine()->scanRows(array($this, '_getAttackDataRowsSerialized')); return wfWAFUtils::json_encode($this->attackDataRows); } /** * @return array * @throws wfWAFStorageFileException */ public function getAttackDataArray() { $this->open(); $this->attackDataRows = array(); $this->getAttackDataEngine()->scanRows(array($this, '_getAttackDataRows')); return $this->attackDataRows; } /** * @param resource $fileHandle * @param int $offset * @param int $length */ public function _getAttackDataRowsSerialized($fileHandle, $offset, $length) { fseek($fileHandle, $offset); self::lock($fileHandle, LOCK_SH); $binary = fread($fileHandle, $length); self::lock($fileHandle, LOCK_UN); $row = wfWAFAttackDataStorageFileEngineRow::unpack($binary); $data = wfWAFUtils::json_decode($row->getData(), true); if (is_array($data)) { array_unshift($data, $row->getTimestamp()); $this->attackDataRows[] = $data; } } /** * @param resource $fileHandle * @param int $offset * @param int $length */ public function _getAttackDataRows($fileHandle, $offset, $length) { fseek($fileHandle, $offset); self::lock($fileHandle, LOCK_SH); $binary = fread($fileHandle, $length); self::lock($fileHandle, LOCK_UN); $row = wfWAFAttackDataStorageFileEngineRow::unpack($binary); $data = $this->unserializeRow($row->getData()); array_unshift($data, $row->getTimestamp()); $this->attackDataRows[] = $data; } /** * @param $newerThan * @return array * @throws wfWAFStorageFileException */ public function getNewestAttackDataArray($newerThan) { $this->open(); $this->attackDataRows = array(); $this->attackDataNewerThan = $newerThan; $this->getAttackDataEngine()->scanRowsReverse(array($this, '_getAttackDataRowsNewerThan')); return $this->attackDataRows; } /** * @param resource $fileHandle * @param int $offset * @param int $length * @return bool */ public function _getAttackDataRowsNewerThan($fileHandle, $offset, $length) { fseek($fileHandle, $offset); self::lock($fileHandle, LOCK_SH); $binaryTimestamp = fread($fileHandle, 8); self::lock($fileHandle, LOCK_UN); $timestamp = wfWAFAttackDataStorageFileEngine::unpackMicrotime($binaryTimestamp); if ($timestamp > $this->attackDataNewerThan) { $binary = $binaryTimestamp . fread($fileHandle, $length - 8); $row = wfWAFAttackDataStorageFileEngineRow::unpack($binary); $data = $this->unserializeRow($row->getData()); if (is_array($data)) { array_unshift($data, $row->getTimestamp()); $this->attackDataRows[] = $data; } return true; } return false; } /** * @return bool * @throws wfWAFStorageFileException */ public function truncateAttackData() { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $this->open(); $this->getAttackDataEngine()->truncate(); return $this->getAttackDataEngine()->getRowCount() === 0; } /** * @return bool * @throws wfWAFStorageFileException */ public function isAttackDataFull() { $this->open(); return $this->getAttackDataEngine()->getRowCount() === wfWAFAttackDataStorageFileEngine::MAX_ROWS; } /** * @param array $failedRules * @param string $failedParamKey * @param string $failedParamValue * @param wfWAFRequestInterface $request * @param mixed $_ * @return mixed */ public function logAttack($failedRules, $failedParamKey, $failedParamValue, $request, $_ = null) { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $this->open(); $row = array( $request->getTimestamp(), $request->getIP(), (int) $this->isInLearningMode(), $failedParamKey, $failedParamValue, ); $failedRulesString = ''; if (is_array($failedRules)) { /** * @var int $index * @var wfWAFRule|int $rule */ foreach ($failedRules as $index => $rule) { if ($rule instanceof wfWAFRule) { $failedRulesString .= $rule->getRuleID() . '|'; } else { $failedRulesString .= $rule . '|'; } } $failedRulesString = wfWAFUtils::substr($failedRulesString, 0, -1); } $row[] = $failedRulesString; $row[] = $request->getProtocol() === 'https' ? 1 : 0; $row[] = (string) $request; // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection $args = func_get_args(); $row = array_merge($row, array_slice($args, 4)); if (($rowString = $this->serializeRow($row)) !== false) { $attackRow = new wfWAFAttackDataStorageFileEngineRow(microtime(false), $rowString); $this->getAttackDataEngine()->addRow($attackRow); } } /** * @param int $timestamp * @param string $ip * @return mixed|void * @throws wfWAFStorageFileException */ public function blockIP($timestamp, $ip, $type = wfWAFStorageInterface::IP_BLOCKS_SINGLE) { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $this->open(); if (!$this->isIPBlocked($ip)) { self::lock($this->ipCacheFileHandle, LOCK_EX); fseek($this->ipCacheFileHandle, 0, SEEK_END); fwrite($this->ipCacheFileHandle, wfWAFUtils::inet_pton($ip) . pack('V', $timestamp) . pack('V', $type)); fflush($this->ipCacheFileHandle); self::lock($this->ipCacheFileHandle, LOCK_UN); } } /** * @param string $ip * @return bool */ public function isIPBlocked($ip) { $this->open(); $ipBin = wfWAFUtils::inet_pton($ip); fseek($this->ipCacheFileHandle, wfWAFUtils::strlen(self::LOG_FILE_HEADER), SEEK_SET); self::lock($this->ipCacheFileHandle, LOCK_SH); while (!feof($this->ipCacheFileHandle)) { $ipStr = fread($this->ipCacheFileHandle, self::IP_BLOCK_RECORD_SIZE); if (wfWAFUtils::strlen($ipStr) < self::IP_BLOCK_RECORD_SIZE) { break; } $ip2 = wfWAFUtils::substr($ipStr, 0, 16); $unpacked = @unpack('V', wfWAFUtils::substr($ipStr, 16, 4)); if (is_array($unpacked)) { $t = array_shift($unpacked); if ($ipBin === $ip2 && $t >= time()) { self::lock($this->ipCacheFileHandle, LOCK_UN); return true; } } } self::lock($this->ipCacheFileHandle, LOCK_UN); return false; } public function pathForConfig($category = '') { $category = strtolower(preg_replace('/[^a-z0-9]/i', '', $category)); $path = $this->_normalizeSlashes($this->getConfigFile()); $components = explode('/', $path); $last = $components[count($components) - 1]; if (preg_match('/^([^.]+)(\..+$|$)/', $last, $matches) && !empty($category)) { $last = $matches[1] . '-' . $category . $matches[2]; } $components[count($components) - 1] = $last; $path = implode('/', $components); return $path; } /** * @return bool */ public function isOpened($category = '') { return isset($this->configFileHandles[$category]) && is_resource($this->configFileHandles[$category]); } /** * @throws wfWAFStorageFileException */ public function open($category = '') { if ($this->isOpened($category)) { return; } if ($this->uninstalled) { throw new wfWAFStorageFileException('Unable to open WAF file storage, WAF has been uninstalled.'); } if (!empty($category)) { //A non-empty category only opens that config file rather than including the rest of the WAF files. $this->_open($this->pathForConfig($category), $this->configFileHandles, $category, self::LOG_FILE_HEADER . self::LOG_INFO_HEADER . serialize($this->getDefaultConfiguration($category)), true); return; } $files = array( array($this->getIPCacheFile(), 'ipCacheFileHandle', false, self::LOG_FILE_HEADER, true), array($this->pathForConfig($category), 'configFileHandles', $category, self::LOG_FILE_HEADER . self::LOG_INFO_HEADER . serialize($this->getDefaultConfiguration($category)), false), ); foreach ($files as $file) { list($filePath, $fileHandle, $arrayKey, $defaultContents, $remakeIfCorrupt) = $file; $this->_open($filePath, $this->$fileHandle, $arrayKey, $defaultContents, $remakeIfCorrupt); } $this->setAttackDataEngine(new wfWAFAttackDataStorageFileEngine($this->getAttackDataFile())); $this->getAttackDataEngine()->open(); } private function _open($filePath, &$fileHandle, $arrayKey, $defaultContents, $remakeIfCorrupt = false) { if (!file_exists($filePath)) { @file_put_contents($filePath, $defaultContents, LOCK_EX); } if (wfWAFStorageFile::allowFileWriting()) { @chmod($filePath, self::permissions()); } if ($arrayKey !== false) { $fileHandle[$arrayKey] = @fopen($filePath, 'r+'); $handle = $fileHandle[$arrayKey]; } else { $fileHandle = @fopen($filePath, 'r+'); $handle = $fileHandle; } if (!$handle && $remakeIfCorrupt && wfWAFStorageFile::allowFileWriting()) { @file_put_contents($filePath, $defaultContents, LOCK_EX); @chmod($filePath, self::permissions()); if ($arrayKey !== false) { $fileHandle[$arrayKey] = @fopen($filePath, 'r+'); $handle = $fileHandle[$arrayKey]; } else { $fileHandle = @fopen($filePath, 'r+'); $handle = $fileHandle; } } if (!$handle) { throw new wfWAFStorageFileException('Unable to open ' . $filePath . ' for reading and writing.'); } } /** * */ public function close($category = '') { if (!$this->isOpened($category)) { return; } fclose($this->configFileHandles[$category]); unset($this->configFileHandles[$category]); if (!empty($category)) { //A non-empty category only closes that config file rather than including the rest of the WAF files. return; } fclose($this->ipCacheFileHandle); $this->ipCacheFileHandle = null; $this->getAttackDataEngine()->close(); } /** * Clean up old expired IP blocks. */ public function vacuum() { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $this->open(); $readPointer = wfWAFUtils::strlen(self::LOG_FILE_HEADER); $writePointer = wfWAFUtils::strlen(self::LOG_FILE_HEADER); fseek($this->ipCacheFileHandle, $readPointer, SEEK_SET); self::lock($this->ipCacheFileHandle, LOCK_EX); $ipCacheRow = fread($this->ipCacheFileHandle, self::IP_BLOCK_RECORD_SIZE); while (!feof($this->ipCacheFileHandle)) { $unpacked = @unpack('V', wfWAFUtils::substr($ipCacheRow, 16, 4)); if (is_array($unpacked)) { $expires = array_shift($unpacked); if ($expires >= time()) { fseek($this->ipCacheFileHandle, $writePointer, SEEK_SET); fwrite($this->ipCacheFileHandle, $ipCacheRow); $writePointer += self::IP_BLOCK_RECORD_SIZE; } } $readPointer += self::IP_BLOCK_RECORD_SIZE; fseek($this->ipCacheFileHandle, $readPointer, SEEK_SET); $ipCacheRow = fread($this->ipCacheFileHandle, self::IP_BLOCK_RECORD_SIZE); } ftruncate($this->ipCacheFileHandle, $writePointer); fflush($this->ipCacheFileHandle); self::lock($this->ipCacheFileHandle, LOCK_UN); } /** * Remove all existing IP blocks. */ public function purgeIPBlocks($types = wfWAFStorageInterface::IP_BLOCKS_ALL) { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $this->open(); $readPointer = wfWAFUtils::strlen(self::LOG_FILE_HEADER); $writePointer = wfWAFUtils::strlen(self::LOG_FILE_HEADER); fseek($this->ipCacheFileHandle, $readPointer, SEEK_SET); self::lock($this->ipCacheFileHandle, LOCK_EX); if ($types !== wfWAFStorageInterface::IP_BLOCKS_ALL) { $ipCacheRow = fread($this->ipCacheFileHandle, self::IP_BLOCK_RECORD_SIZE); while (!feof($this->ipCacheFileHandle)) { $unpacked = @unpack('Vexpires/Vtype', wfWAFUtils::substr($ipCacheRow, 16, 8)); if (is_array($unpacked)) { $type = $unpacked['type']; if (($type & $types) == 0) { fseek($this->ipCacheFileHandle, $writePointer, SEEK_SET); fwrite($this->ipCacheFileHandle, $ipCacheRow); $writePointer += self::IP_BLOCK_RECORD_SIZE; } } $readPointer += self::IP_BLOCK_RECORD_SIZE; fseek($this->ipCacheFileHandle, $readPointer, SEEK_SET); $ipCacheRow = fread($this->ipCacheFileHandle, self::IP_BLOCK_RECORD_SIZE); } } ftruncate($this->ipCacheFileHandle, $writePointer); fflush($this->ipCacheFileHandle); self::lock($this->ipCacheFileHandle, LOCK_UN); } /** * @param string $key * @param mixed $default * @return mixed */ public function getConfig($key, $default = null, $category = '') { if (!isset($this->data[$category]) || $this->data[$category] === false) { $this->fetchConfigData($category); } return (is_array($this->data[$category]) && array_key_exists($key, $this->data[$category])) ? $this->data[$category][$key] : $default; } /** * @param string $key * @param mixed $value */ public function setConfig($key, $value, $category = '') { if (!isset($this->data[$category]) || $this->data[$category] === false) { $this->fetchConfigData($category); } if (is_array($this->data[$category])) { if (!isset($this->dataChanged[$category]) && ( (array_key_exists($key, $this->data[$category]) && $this->data[$category][$key] !== $value) || !array_key_exists($key, $this->data[$category]) ) ) { $this->dataChanged[$category] = array($key, true); register_shutdown_function(array($this, 'saveConfig'), $category); } $this->data[$category][$key] = $value; } } /** * @param string $key */ public function unsetConfig($key, $category = '') { if (!isset($this->data[$category]) || $this->data[$category] === false) { $this->fetchConfigData($category); } if (!isset($this->dataChanged[$category]) && is_array($this->data[$category]) && array_key_exists($key, $this->data[$category])) { $this->dataChanged[$category] = array($key, true); register_shutdown_function(array($this, 'saveConfig'), $category); } unset($this->data[$category][$key]); } /** * @throws wfWAFStorageFileException */ public function fetchConfigData($category = '', $redoing = false) { unset($this->configFileHandles[$category]); $this->open($category); self::lock($this->configFileHandles[$category], LOCK_SH); $i = 0; // Attempt to read contents of the config file. This could be in the middle of a write, so we account for it and // wait for the operation to complete. fseek($this->configFileHandles[$category], wfWAFUtils::strlen(self::LOG_FILE_HEADER), SEEK_SET); $serializedData = ''; while (!feof($this->configFileHandles[$category])) { $serializedData .= fread($this->configFileHandles[$category], 1024); } if (wfWAFUtils::substr($serializedData, 0, 1) == '*') { $serializedData = wfWAFUtils::substr($serializedData, wfWAFUtils::strlen(self::LOG_INFO_HEADER)); } $this->data[$category] = @unserialize($serializedData); if ($this->data[$category] === false) { if (!empty($category) && !$redoing) { $this->regenerateConfigFile($category); $this->fetchConfigData($category, true); return; } throw new wfWAFStorageFileConfigException('Error reading Wordfence Firewall config data, configuration file could be corrupted or inaccessible. Path: ' . $this->pathForConfig($category)); } self::lock($this->configFileHandles[$category], LOCK_UN); } /** * @throws wfWAFStorageFileException */ public function saveConfig($category = '') { if (!wfWAFStorageFile::allowFileWriting()) { return false; } if (WFWAF_DEBUG) { error_log('Saving WAF config for change in key ' . $this->dataChanged[$category][0] . ', value: ' . ((is_object($this->data[$category][$this->dataChanged[$category][0]]) || $this->dataChanged[$category][0] === 'cron') ? gettype($this->data[$category][$this->dataChanged[$category][0]]) : var_export($this->data[$category][$this->dataChanged[$category][0]], true))); } if ($this->uninstalled) { return; } if (WFWAF_IS_WINDOWS) { self::lock($this->configFileHandles[$category], LOCK_UN); fclose($this->configFileHandles[$category]); file_put_contents($this->pathForConfig($category), self::LOG_FILE_HEADER . self::LOG_INFO_HEADER . serialize($this->data[$category]), LOCK_EX); } else { wfWAFStorageFile::atomicFilePutContents($this->pathForConfig($category), self::LOG_FILE_HEADER . self::LOG_INFO_HEADER . serialize($this->data[$category])); } if (WFWAF_IS_WINDOWS) { $this->configFileHandles[$category] = fopen($this->pathForConfig($category), 'r+'); } } /** * */ public function uninstall() { $this->uninstalled = true; $this->close(); foreach ($this->configFileHandles as $key => $handle) { $this->close($key); } $this->removeConfigFiles(); @unlink($this->getAttackDataFile()); @unlink($this->getIPCacheFile()); @unlink($this->getRulesDSLCacheFile()); } public function fileList() { $fileList = array(); $fileList[] = $this->getAttackDataFile(); $fileList[] = $this->getIPCacheFile(); if (defined('WFWAF_DEBUG') && WFWAF_DEBUG) { $fileList[] = $this->getRulesDSLCacheFile(); } $fileList[] = $this->getConfigFile(); $configDir = dirname($this->getConfigFile()); $dir = opendir($configDir); if ($dir) { $escapedPath = preg_quote($this->_normalizeSlashes($this->getConfigFile()), '/'); $components = explode('\\/', $escapedPath); $pattern = $components[count($components) - 1]; if (preg_match('/^(.+?)(\\\..+$|$)/i', $pattern, $matches)) { $pattern = $matches[1] . '\\-[a-z0-9]+' . $matches[2]; //Results in a pattern like config\-[a-z0-9]\.php } while ($path = readdir($dir)) { if ($path == '.' || $path == '..') { continue; } if (is_dir($configDir . '/' . $path)) { continue; } if (preg_match('/^' . $pattern . '$/i', $path)) { $fileList[] = $configDir . '/' . $path; } } closedir($dir); } return $fileList; } public function removeConfigFiles() { @unlink($this->getConfigFile()); $configDir = dirname($this->getConfigFile()); $dir = opendir($configDir); if ($dir) { $escapedPath = preg_quote($this->_normalizeSlashes($this->getConfigFile()), '/'); $components = explode('\\/', $escapedPath); $pattern = $components[count($components) - 1]; if (preg_match('/^(.+?)(\\\..+$|$)/i', $pattern, $matches)) { $pattern = $matches[1] . '\\-[a-z0-9]+' . $matches[2]; //Results in a pattern like config\-[a-z0-9]\.php } while ($path = readdir($dir)) { if ($path == '.' || $path == '..') { continue; } if (is_dir($configDir . '/' . $path)) { continue; } if (preg_match('/^' . $pattern . '$/i', $path)) { @unlink($configDir . '/' . $path); } } closedir($dir); } } public function regenerateConfigFile($category = '') { $path = $this->pathForConfig($category); if (file_exists($path)) { @unlink($path); } $this->_open($path, $this->configFileHandles, $category, self::LOG_FILE_HEADER . self::LOG_INFO_HEADER . serialize($this->getDefaultConfiguration($category)), false); } protected function _normalizeSlashes($path) { return str_replace('\\', '/', $path); //The same sanitation performed by WordPress -- PHP can handle both, but it simplifies path processing } /** * @return bool */ public function isInLearningMode() { if ($this->getConfig('wafStatus', '') == 'learning-mode') { if ($this->getConfig('learningModeGracePeriodEnabled', false)) { if ($this->getConfig('learningModeGracePeriod', 0) > time()) { return true; } } else { return true; } } return false; } public function isDisabled() { return $this->getConfig('wafStatus', '') === 'disabled' || $this->getConfig('wafDisabled', 0); } /** * @return array */ public function getDefaultConfiguration($category = '') { if (empty($category)) { return array( 'wafStatus' => 'learning-mode', 'learningModeGracePeriodEnabled' => 1, 'learningModeGracePeriod' => time() + (86400 * 7), 'authKey' => wfWAFUtils::getRandomString(64), ); } return array(); } /** * @return mixed */ public function getConfigFile() { return $this->configFile; } /** * @param mixed $configFile */ public function setConfigFile($configFile) { $this->configFile = $configFile; } /** * @return string|null */ public function getAttackDataFile() { return $this->attackDataFile; } /** * @param string|null $attackDataFile */ public function setAttackDataFile($attackDataFile) { $this->attackDataFile = $attackDataFile; } /** * @return string|null */ public function getIPCacheFile() { return $this->ipCacheFile; } /** * @param string|null $ipCacheFile */ public function setIPCacheFile($ipCacheFile) { $this->ipCacheFile = $ipCacheFile; } /** * @return mixed */ public function getRulesDSLCacheFile() { return $this->rulesDSLCacheFile; } /** * @param mixed $rulesDSLCacheFile */ public function setRulesDSLCacheFile($rulesDSLCacheFile) { $this->rulesDSLCacheFile = $rulesDSLCacheFile; } /** * param key, param value, request string * * @var array */ private $rowsToB64 = array(3, 4, 7); /** * @param $row * @return bool|string */ private function serializeRow($row) { foreach ($this->rowsToB64 as $index) { if (array_key_exists($index, $row)) { $row[$index] = base64_encode((string) $row[$index]); } } $row = wfWAFUtils::json_encode($row); if (is_string($row) && wfWAFUtils::strlen($row) > 0) { return $row; } return false; } /** * @param $row * @return array|bool|mixed|object */ private function unserializeRow($row) { if ($row) { $json = wfWAFUtils::json_decode($row, true); if (is_array($json)) { foreach ($this->rowsToB64 as $index) { if (array_key_exists($index, $json)) { $json[$index] = base64_decode((string) $json[$index]); } } return $json; } } return false; } /** * @return wfWAFAttackDataStorageFileEngine */ public function getAttackDataEngine() { return $this->attackDataEngine; } /** * @param wfWAFAttackDataStorageFileEngine $attackDataEngine */ public function setAttackDataEngine($attackDataEngine) { $this->attackDataEngine = $attackDataEngine; } public function getRules() { throw new wfWAFStorageFileException('wfWAFStorageFile::getRules not implemented.'); } public function setRules($rules) { throw new wfWAFStorageFileException('wfWAFStorageFile::getRules not implemented.'); } public function needsInitialRules() { if (file_exists($this->getRulesFile())) { return is_writeable($this->getRulesFile()) && !filesize($this->getRulesFile()); } else { return is_writeable(dirname($this->getRulesFile())); } } /** * @return mixed */ public function getRulesFile() { return $this->rulesFile; } /** * @param mixed $rulesFile */ public function setRulesFile($rulesFile) { $this->rulesFile = $rulesFile; } public function getDescription() { return __('file system', 'wordfence'); } } class wfWAFAttackDataStorageFileEngine { const MAX_ROWS = 10000; const MAX_READ_LENGTH = 51200; const FILE_SIGNATURE = "wfWAF\x00\x00\x00"; /** * @param string|float|null $microtime * @return string */ public static function packMicrotime($microtime = null) { if ($microtime === null) { $microtime = microtime(); } if (is_string($microtime)) { list($msec, $sec) = explode(' ', $microtime, 2); } else if (is_float($microtime)) { list($sec, $msec) = explode('.', (string) $microtime, 2); $msec = '0.' . $msec; } else { throw new InvalidArgumentException(__METHOD__ . ' $microtime expected to be string or float, received ' . gettype($microtime)); } $msec = $msec * 1000000; return pack('V*', $sec, $msec); } /** * @param string $binary * @return string */ public static function unpackMicrotime($binary) { if (!is_string($binary) || wfWAFUtils::strlen($binary) !== 8) { throw new InvalidArgumentException(__METHOD__ . ' $binary expected to be string with length of 8, received ' . gettype($binary) . (is_string($binary) ? ' of length ' . wfWAFUtils::strlen($binary) : '')); } list(, $attackLogSeconds, $attackLogMicroseconds) = @unpack('V*', $binary); return sprintf('%d.%s', $attackLogSeconds, str_pad($attackLogMicroseconds, 6, '0', STR_PAD_LEFT)); } public static function getCompressionAlgos() { static $compressionFunctions; if ($compressionFunctions === null) { $compressionFunctions = array( new wfWAFStorageFileCompressionGZDeflate(), new wfWAFStorageFileCompressionGZCompress(), new wfWAFStorageFileCompressionGZEncode(), ); } return $compressionFunctions; } /** * @param string $decompressed * @return mixed */ public static function compress($decompressed) { if (empty($decompressed)) return $decompressed; $compressionAlgos = self::getCompressionAlgos(); /** @var wfWAFStorageFileCompressionAlgo $algo */ foreach ($compressionAlgos as $algo) { if ($algo->isUsable() && ($compressed = $algo->testCompression($decompressed)) !== false) { return $compressed; } } return $decompressed; } /** * @param string $compressed * @return mixed */ public static function decompress($compressed) { if (empty($compressed)) return $compressed; $compressionAlgos = self::getCompressionAlgos(); /** @var wfWAFStorageFileCompressionAlgo $algo */ foreach ($compressionAlgos as $algo) { if ($algo->isUsable() && ($decompressed = $algo->decompress($compressed)) !== false) { return $decompressed; } } return $compressed; } private $file; private $fileHandle; private $header = array(); private $offsetTable = array(); /** * wfWAFStorageFileEngine constructor. * @param string $file */ public function __construct($file) { $this->file = $file; } /** * @throws wfWAFStorageFileException */ public function open() { if (is_resource($this->fileHandle)) { return; } if (!file_exists($this->file)) { @file_put_contents($this->file, $this->getDefaultHeader(), LOCK_EX); } if (wfWAFStorageFile::allowFileWriting()) { @chmod($this->file, wfWAFStorageFile::permissions()); } $this->fileHandle = @fopen($this->file, 'r+'); if (!$this->fileHandle) { throw new wfWAFStorageFileException('Unable to open ' . $this->file . ' for reading and writing.'); } } /** * */ public function close() { if (is_resource($this->fileHandle)) { fclose($this->fileHandle); } $this->fileHandle = null; $this->header = array(); $this->offsetTable = array(); } /** * @param int $offset * @return int */ private function seek($offset) { return fseek($this->fileHandle, $offset, SEEK_SET); } /** * @return int */ private function seekToData() { return $this->seek(wfWAFUtils::strlen($this->getHeaderLength())); } /** * @param int $length * @return string */ private function read($length) { if ($length > self::MAX_READ_LENGTH) { $length = self::MAX_READ_LENGTH; } return fread($this->fileHandle, $length); } /** * @param string $data * @return int */ private function write($data) { return fwrite($this->fileHandle, $data); } /** * @return bool */ private function lockRead() { return wfWAFStorageFile::lock($this->fileHandle, LOCK_SH); } /** * @return bool */ private function lockWrite() { return wfWAFStorageFile::lock($this->fileHandle, LOCK_EX); } /** * @return bool */ private function unlock() { return wfWAFStorageFile::lock($this->fileHandle, LOCK_UN); } /** * @return int */ public function getHeaderLength() { return wfWAFUtils::strlen($this->getDefaultHeader()); } /** * @return string */ public function getDefaultHeader() { /** * 51 PHP die() header * 8 Signature * 8 oldest 64bit timestamp * 8 newest 64bit timestamp * 4 row count * 1600 offset table * 1 last length */ $headerLength = wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8 + 4 + (self::MAX_ROWS * 4); return wfWAFStorageFile::LOG_FILE_HEADER . self::FILE_SIGNATURE . str_repeat("\x00", 8 + 8 + 4) . pack('V', $headerLength) . str_repeat("\x00", self::MAX_ROWS * 4); } /** * @throws wfWAFStorageFileException */ public function unpackHeader() { if ($this->header) { return $this->header; } $this->open(); $this->header = array(); $this->seek(0); $this->lockRead(); $this->header['phpHeader'] = $this->read(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER)); $this->header['signature'] = $this->read(wfWAFUtils::strlen(self::FILE_SIGNATURE)); if ($this->header['phpHeader'] !== wfWAFStorageFile::LOG_FILE_HEADER || $this->header['signature'] !== self::FILE_SIGNATURE) { $this->unlock(); $this->truncate(); $this->lockRead(); $this->seek(0); $this->lockRead(); $this->header['phpHeader'] = $this->read(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER)); $this->header['signature'] = $this->read(wfWAFUtils::strlen(self::FILE_SIGNATURE)); } $this->header['oldestTimestamp'] = self::unpackMicrotime($this->read(8)); $this->header['newestTimestamp'] = self::unpackMicrotime($this->read(8)); list(, $this->header['rowCount']) = @unpack('V', $this->read(4)); $this->header['offsetTable'] = $this->unpackOffsetTable(); $this->unlock(); return $this->header; } /** * @return array */ private function unpackOffsetTable() { if ($this->offsetTable) { return $this->offsetTable; } $rowCount = min($this->header['rowCount'], self::MAX_ROWS); $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8 + 4); $offsetTableBinary = $this->read(($rowCount + 1) * 4); $this->offsetTable = array_values(@unpack('V*', $offsetTableBinary)); return $this->offsetTable; } /** * @param callable $callback */ public function scanRows($callback) { if (!is_callable($callback)) { throw new InvalidArgumentException(__METHOD__ . ' $callback expected to be callable, received ' . gettype($callback)); } $this->open(); $header = $this->unpackHeader(); $this->seekToData(); for ($index = 0; $index < $header['rowCount'] && $index < self::MAX_ROWS; $index++) { $offset = $header['offsetTable'][$index]; $length = $header['offsetTable'][$index + 1] - $offset; if ($length > self::MAX_READ_LENGTH) { $length = self::MAX_READ_LENGTH; } $result = call_user_func($callback, $this->fileHandle, $offset, $length); if ($result === false) { break; } } } /** * @param callable $callback */ public function scanRowsReverse($callback) { if (!is_callable($callback)) { throw new InvalidArgumentException(__METHOD__ . ' $callback expected to be callable, received ' . gettype($callback)); } $this->open(); $header = $this->unpackHeader(); // $this->seekToData(); for ($index = min($header['rowCount'], self::MAX_ROWS) - 1; $index >= 0; $index--) { $offset = $header['offsetTable'][$index]; $length = $header['offsetTable'][$index + 1] - $offset; if ($length > self::MAX_READ_LENGTH) { $length = self::MAX_READ_LENGTH; } $result = call_user_func($callback, $this->fileHandle, $offset, $length); if ($result === false) { break; } } } /** * @param $index * @return wfWAFAttackDataStorageFileEngineRow * @throws wfWAFStorageFileException */ public function getRow($index) { $this->open(); $this->header = array(); $this->offsetTable = array(); $header = $this->unpackHeader(); $this->seekToData(); if ($index < $header['rowCount'] && $index >= 0) { $offset = $header['offsetTable'][$index]; $length = $header['offsetTable'][$index + 1] - $offset; } else { return false; } $this->seek($offset); $this->lockRead(); $binary = $this->read($length); $this->unlock(); return wfWAFAttackDataStorageFileEngineRow::unpack($binary); } /** * @return mixed * @throws wfWAFStorageFileException */ public function getRowCount() { $this->open(); $header = $this->unpackHeader(); return $header['rowCount']; } /** * @param wfWAFAttackDataStorageFileEngineRow $row * @return bool * @throws wfWAFStorageFileException */ public function addRow($row) { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $this->open(); $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8); $this->lockRead(); list(, $rowCount) = @unpack('V', $this->read(4)); $this->unlock(); if ($rowCount >= self::MAX_ROWS) { return false; } $this->lockWrite(); //Re-read the row count in case it changed between releasing the shared lock and getting the exclusive $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8); list(, $rowCount) = @unpack('V', $this->read(4)); //Start the write $this->header = array(); $this->offsetTable = array(); $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8 + 4 + ($rowCount * 4)); list(, $nextRowOffset) = @unpack('V', $this->read(4)); $rowString = $row->pack(); // Update offset table $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8 + 4 + (($rowCount + 1) * 4)); $this->write(pack('V', $nextRowOffset + wfWAFUtils::strlen($rowString))); // Update rowCount $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8 + 8); $this->write(pack('V', $rowCount + 1)); // Write data $this->seek($nextRowOffset); $packedTimestamp = wfWAFUtils::substr($rowString, 0, 8); $this->write($rowString); // Update oldest timestamp if ($rowCount === 0) { $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE)); $this->write($packedTimestamp); } // Update newest timestamp $this->seek(wfWAFUtils::strlen(wfWAFStorageFile::LOG_FILE_HEADER) + wfWAFUtils::strlen(self::FILE_SIGNATURE) + 8); $this->write($packedTimestamp); $this->unlock(); $this->header = array(); $this->offsetTable = array(); return true; } /** * */ public function truncate() { if (!wfWAFStorageFile::allowFileWriting()) { return false; } $defaultHeader = $this->getDefaultHeader(); $this->close(); if (WFWAF_IS_WINDOWS) { file_put_contents($this->getFile(), $defaultHeader, LOCK_EX); } else { wfWAFStorageFile::atomicFilePutContents($this->getFile(), $defaultHeader, 'attack'); } $this->header = array(); $this->offsetTable = array(); $this->open(); } /** * @return mixed * @throws wfWAFStorageFileException */ public function getOldestTimestamp() { $this->open(); if ($this->getRowCount() === 0) { return false; } $header = $this->unpackHeader(); return $header['oldestTimestamp']; } /** * @return mixed * @throws wfWAFStorageFileException */ public function getNewestTimestamp() { $this->open(); if ($this->getRowCount() === 0) { return false; } $header = $this->unpackHeader(); return $header['newestTimestamp']; } /** * @return string */ public function getFile() { return $this->file; } /** * @param string $file */ public function setFile($file) { $this->file = $file; } } interface wfWAFAttackDataStorageFileEngineScanRowCallback { public function scanRow($handle, $offset, $length); } class wfWAFAttackDataStorageFileEngineResultSet implements wfWAFAttackDataStorageFileEngineScanRowCallback { private $rows = array(); public function scanRow($handle, $offset, $length) { fseek($handle, $offset); $binary = fread($handle, $length); $this->rows = wfWAFAttackDataStorageFileEngineRow::unpack($binary); } /** * @return array */ public function getRows() { return $this->rows; } } class wfWAFAttackDataStorageFileEngineScanRowAttackDataNewer implements wfWAFAttackDataStorageFileEngineScanRowCallback { /** * @var int */ private $newerThan; /** * wfWAFStorageFileEngineScanRowAttackDataNewer constructor. * @param int $newerThan */ public function __construct($newerThan) { $this->newerThan = $newerThan; } /** * @param resource $handle * @param int $offset * @param int $length * @return bool */ public function scanRow($handle, $offset, $length) { $attackLogTimeBin = fread($handle, 8); list(, $attackLogSeconds, $attackLogMicroseconds) = @unpack('VV', $attackLogTimeBin); $attackLogTime = $attackLogSeconds . '.' . $attackLogMicroseconds; return $this->newerThan < $attackLogTime; } } class wfWAFAttackDataStorageFileEngineScanRowAttackDataOlder implements wfWAFAttackDataStorageFileEngineScanRowCallback { /** * @var int */ private $olderThan; /** * wfWAFStorageFileEngineScanRowAttackDataNewer constructor. * @param int $olderThan */ public function __construct($olderThan) { $this->olderThan = $olderThan; } /** * @param resource $handle * @param int $offset * @param int $length * @return bool */ public function scanRow($handle, $offset, $length) { $attackLogTimeBin = fread($handle, 8); list(, $attackLogSeconds, $attackLogMicroseconds) = @unpack('VV', $attackLogTimeBin); $attackLogTime = $attackLogSeconds . '.' . $attackLogMicroseconds; return $this->olderThan > $attackLogTime; } } class wfWAFAttackDataStorageFileEngineRow { /** * @param string $binary * @return wfWAFAttackDataStorageFileEngineRow */ public static function unpack($binary) { $attackLogTime = wfWAFAttackDataStorageFileEngine::unpackMicrotime(wfWAFUtils::substr($binary, 0, 8)); $data = wfWAFAttackDataStorageFileEngine::decompress(wfWAFUtils::substr($binary, 8)); return new self($attackLogTime, $data); } /** * @var float|string */ private $timestamp; /** * @var string */ private $data; /** * @param float $timestamp * @param string $data */ public function __construct($timestamp, $data) { $this->timestamp = $timestamp; $this->data = $data; } /** * @return string */ public function pack() { return wfWAFAttackDataStorageFileEngine::packMicrotime($this->getTimestamp()) . wfWAFAttackDataStorageFileEngine::compress($this->getData()); } /** * @return float|string */ public function getTimestamp() { return $this->timestamp; } /** * @param float|string $timestamp */ public function setTimestamp($timestamp) { $this->timestamp = $timestamp; } /** * @return string */ public function getData() { return $this->data; } /** * @param string $data */ public function setData($data) { $this->data = $data; } } abstract class wfWAFStorageFileCompressionAlgo { abstract public function isUsable(); abstract public function compress($string); abstract public function decompress($binary); /** * @param string $string * @return bool */ public function testCompression($string) { $compressed = $this->compress($string); if ($string === $this->decompress($compressed)) { return $compressed; } return false; } } class wfWAFStorageFileCompressionGZDeflate extends wfWAFStorageFileCompressionAlgo { public function isUsable() { return function_exists('gzinflate') && function_exists('gzdeflate'); } public function compress($string) { return @gzdeflate($string); } public function decompress($binary) { return @gzinflate($binary); } } class wfWAFStorageFileCompressionGZCompress extends wfWAFStorageFileCompressionAlgo { public function isUsable() { return function_exists('gzuncompress') && function_exists('gzcompress'); } public function compress($string) { return @gzcompress($string); } public function decompress($binary) { return @gzuncompress($binary); } } class wfWAFStorageFileCompressionGZEncode extends wfWAFStorageFileCompressionAlgo { public function isUsable() { return function_exists('gzencode') && function_exists('gzdecode'); } public function compress($string) { return @gzencode($string); } public function decompress($binary) { return @gzdecode($binary); } } class wfWAFStorageFileException extends wfWAFException { } class wfWAFStorageFileConfigException extends wfWAFStorageFileException { } }