Changing Cloudflare Security Level Programmatically
Sometimes one of my sites is under attack from a click-fraud campaign. I needed to devise a way to detect such an attack and instantly and automatically change my Cloudflare security level from ‘medium’ to ‘under attack’. When in under-attack mode, Cloudflare performs additional browser checks to filter out robots. It doesn’t stop all the attacks, but it’s my duty to try something if options are available. To that end, here is a PHP snippet I use to change my site’s security level via a URI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | <?php /** * Eric Draken * Date: 2016-03-02 * Time: 10:41 PM * Desc: Change the Cloudflare security level for a given website */ require_once 'includes/common.php'; // Get the desired domain name $domain = filter_var(strip_tags(trim(@$_GET['domain'])), FILTER_SANITIZE_URL); $level = filter_var(strip_tags(trim(@$_GET['level'])), FILTER_SANITIZE_STRING); // Check level first switch($level) { case 'under_attack': case 'high': case 'medium': case 'low': case 'essentially_off': break; default: $level = 'medium'; break; } $email = API_EMAIL; $apikey = API_KEY; // Get the zone from the domain $zone = file_get_contents('http://'.$_SERVER['SERVER_NAME'].'/cloudflare/getzoneid.php?domain=' . $domain); if(!$zone || strlen($zone) < 32) { die("Bad domain:$domain or zone:$zone"); } $curl = <<<EOD curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{$zone}/settings/security_level" \ -H "X-Auth-Email: {$email}" \ -H "X-Auth-Key: {$apikey}" \ -H "Content-Type: application/json" \ --data '{"value":"{$level}"}' EOD; // Get the response object if($obj = execJSONDecode($curl)) { // Check if valid if($obj->success == true) { // Check if the level returned is correct if($obj->result && $obj->result->value) { $mode = $obj->result->value; // Keep track of the current security level change $key = md5('CF_SECURITY_LEVEL_' . $domain); $cacheseconds = 60 * 60; // 1 hour $memcache = new Memcached(); if(!($memcache->addServer('localhost', 11211) && $memcache->set($key, $mode, $cacheseconds))) { throw new Exception("Something went wrong with memcached"); } die($mode); } else { throw new Exception("Bad security request"); } } else { throw new Exception("Bad curl request response"); } } else { throw new Exception("Bad JSON result"); } |
I use memcached
here because another script polls the last security level that was set. This is optional. I decided to use the Herenow curl string because it is formatted like the Cloudflare API examples, and it just works. The common functions are:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | define('API_KEY', '...........'); define('API_EMAIL', '.....@......'); function oneline($curl) { // Make one line return implode(' ', array_map('trim', explode('\\', $curl))); } // Exec the curl and get the JSON function execJSONDecode($curl) { // Make one line $curl = oneline($curl); // Exec and return the results $results = shell_exec($curl); // Parse the JSON if($results) { $json = @json_decode($results); if (json_last_error() === JSON_ERROR_NONE) { // JSON is valid return $json; } else { error_log("JSON error: $results"); return false; } } error_log("Shell exec failed: $curl"); return false; } |
The one tricky thing when dealing with Cloudflare is obtaining your zone ID for a given domain. With another API call we can retrieve the zone ID of a supplied domain like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <?php /** * Eric Draken * Date: 2016-03-02 * Time: 9:38 PM * Desc: Echo the zone string for the given domain if it exists, cache the zone for 1 day */ require_once 'includes/common.php'; // Get the desired domain name $domain = filter_var(strip_tags(trim(@$_GET['domain'])), FILTER_SANITIZE_URL); // Sanity if(!$domain) { die("0"); } // Memcached $keyprefix = 'CF_ZONE_'; $cacheseconds = 60 * 60 * 24; // One day $memcache = new Memcached(); if($cacheconnected = $memcache->addServer('localhost', 11211)) { if($zone = $memcache->get(md5($keyprefix . $domain))) { die($zone); } } // cURL string $email = API_EMAIL; $apikey = API_KEY; $curl = <<<EOD curl -X GET "https://api.cloudflare.com/client/v4/zones?name={$domain}&status=active&page=1&per_page=50&order=status&direction=desc&match=all" \ -H "X-Auth-Email: {$email}" \ -H "X-Auth-Key: {$apikey}" \ -H "Content-Type: application/json" EOD; // Get the response object if($obj = execJSONDecode($curl)) { // Check if valid if($obj->success == true) { // Check if this domain exists or not if($obj->result && count($obj->result) > 0) { // Save as cache $zone = trim($obj->result[0]->id); if($cacheconnected && !$memcache->set(md5($keyprefix . $domain), $zone, $cacheseconds)) { throw new Exception("Failed to save zone cache"); } die($zone); } else { // This domain doesn't exist die("0"); } } else { throw new Exception("Bad curl request response"); } } else { throw new Exception("Bad JSON result"); } |
Again, memcached
is utilized to avoid over-polling Cloudflare for the zone ID. The same common functions are reused. Why this pattern? There are other Cloudflare convenience scripts I have made such as clearing cache or changing access rules. Also, when, say, v5 of the API eventually comes out, it is easy to change the curl string rather than having to modify custom PHP curl options.