bitslip6/bitfire

View on GitHub
firewall/src/bitfire.php

Summary

Maintainability
B
6 hrs
Test Coverage
<?php
/**
 * BitFire PHP based Firewall.
 * Author: BitFire (BitSlip6 company)
 * Distributed under the AGPL license: https://www.gnu.org/licenses/agpl-3.0.en.html
 * Please report issues to: https://github.com/bitslip6/bitfire/issues
 * 
 * main firewall.  holds core data references.
 */

namespace BitFire;

use BitFire\Config as CFG;
use ThreadFin\CacheStorage;
use ThreadFin\Effect;
use ThreadFin\FileMod;
use ThreadFin\Maybe;
use ThreadFin\MaybeBlock;

use const ThreadFin\DAY;

use function BitFire\Pure\json_to_file_effect;
use function BitFirePlugin\is_admin;
use function BitFireSvr\authenticate_tech;
use function ThreadFin\contains;
use function ThreadFin\dbg;
use function ThreadFin\decrypt_tracking_cookie;
use function ThreadFin\en_json;
use function ThreadFin\HTTP\http2;
use function ThreadFin\random_str;
use function ThreadFin\trace;
use function ThreadFin\debug;
use function ThreadFin\get_hidden_file;
use function ThreadFin\get_public;
use function ThreadFin\partial_right;
use function ThreadFin\un_json;
use function ThreadFin\utc_date;
use function ThreadFin\utc_time;

require_once \BitFire\WAF_SRC."bitfire_pure.php";
require_once \BitFire\WAF_SRC."const.php";
require_once \BitFire\WAF_SRC."util.php";
require_once \BitFire\WAF_SRC."storage.php";
require_once \BitFire\WAF_SRC."english.php";
require_once \BitFire\WAF_SRC."botfilter.php";


/**
 * http header abstraction 
 * @package BitFire
 */
class Headers
{
    /** @var string $requested_with  set to XMLHttpRequest for xml http request */
    public $requested_with = '';
    /** @var string $fetch_mode set to sec-fetch-mode (cors, navigate, no-cors, same-origin, websocket) */
    public $fetch_mode = '';
    /** @var string $accept http accept header */
    public $accept;
    /** @var string $content http content type */
    public $content;
    /** @var string $encoding http accept encoding */
    public $encoding;
    /** @var string $dnt do not track header */
    public $dnt;
    /** @var string $upgrade_insecure upgrade insecure request header */
    public $upgrade_insecure;
    /** @var string $referer the referring html page */
    public $referer;
    public $content_type;
}

/**
 * http request abstraction
 * @package BitFire
 */
class Request
{
    public $host;
    public $path;
    public $ip;
    public $method;
    public $port;
    public $scheme;

    public $get;
    public $get_freq = array();
    public $post;
    public $post_len;
    public $post_raw;
    public $post_freq = array();
    public $cookies;

    public $agent;
    /** @var Headers $headers the request headers */
    public $headers;
}


/**
 * Match class used for matching mapping request match DATA to Request DATA
 * @package BitFire
 */
class MatchType
{
    protected $_type;
    protected $_key;
    protected $_value;
    protected $_matched;
    protected $_block_time;
    protected $_match_str;
    protected $_chained;

    const EXACT = 0;
    const CONTAINS = 1;
    const IN = 2;
    const NOTIN = 3;
    const REGEX = 4;

    public function __construct(int $type, string $key, $value, int $block_time, MatchType $chain = null) {
        $this->_type = $type;
        $this->_key = $key;
        $this->_value = $value;
        $this->_matched = 'none';
        $this->_block_time = $block_time;
        $this->_match_str = '';
        $this->_chained = $chain;
    }

    /**
     * Test if the request matches the MatchType
     * 
     * @param Request $request 
     * @return bool 
     */
    public function match(\BitFire\Request $request) : bool {
        $key = $this->_key;
        $this->_matched = $request->$key ?? '';
        $result = false;
        switch ($this->_type) {
            case MatchType::EXACT: 
                $result = ($this->_matched === $this->_value);
                break;
            case MatchType::CONTAINS: 
                if (is_array($this->_value)) {
                    foreach ($this->_value as $v) {
                        $m = strpos($this->_matched, $v);

                        if ($m !== false) { 
                            $result = true;
                            if (is_string($v)) {
                                $this->_match_str = $v;
                            } else {
                                $this->_match_str = json_encode($v);
                            }
                            break;
                        }
                    }
                }
                else { $result = strpos($this->_matched, $this->_value) !== false; }
                break;
            case MatchType::IN: 
                $result = in_array($this->_matched, $this->_value);
                break;
            case MatchType::NOTIN: 
                $result = !in_array($this->_matched, $this->_value);
                break;
            case MatchType::REGEX:
                $result = preg_match($this->_value, $this->_matched) > 0;
                break;
            default:
        }

        // chain additional match types
        if ($result && $this->_chained) {
            $result = $this->_chained->match($request);
        }

        if ($result && $this->_match_str === '') { $this->_match_str = $this->_value; }
        return $result;
    }

    public function match_pattern() : string {
        return $this->_match_str;
    }

    public function matched_data() : string {
        return $this->_matched;
    }

    public function get_field() : string {
        return $this->_key;
    }
}

class Block {

    public $code;
    public $parameter;
    public $value;
    public $pattern;
    public $block_time; // set to -1 for warning, 0 = block this request, 1 = short, 2 = medium 3 = long
    public $skip_reporting = false;
    public $uuid;

    public function __construct(int $code, string $parameter, string $value, string $pattern, int $block_time = 0) {
        $this->code = $code;
        $this->parameter = $parameter;
        $this->value = $value;
        $this->pattern = $pattern;
        $this->block_time = $block_time;
        $this->uuid = strtoupper(random_str(8));
    }
    
    public function __toString() : string {
        $class = intval(floor($this->code/1000)*1000);
        return \BitFire\FEATURE_NAMES[$class]??"Unclassified:{$this->code}";
    }
}

class Exception {
    public $code;
    public $parameter;
    public $url;
    public $host;
    public $uuid;
    public $date;

    public function __construct(int $code = 0, string $uuid = 'x', ?string $parameter = NULL, ?string $url = NULL, ?string $host = NULL) {
        $this->code = $code;
        $this->parameter = $parameter;
        $this->url = $url;
        $this->host = $host;
        $this->uuid = $uuid;
    }
}


class Config {
    public static $_options = null;
    private static $_nonce = null;

    public static function nonce() : string {
        if (self::$_nonce == null) {
            self::$_nonce = str_replace(array('-','+','/'), "", random_str(10));
        }
        return self::$_nonce;
    }

    // set the full list of configuration options
    public static function set(array $options) : void {
        if (empty($options)) {
            trace("no cfg");
            CacheStorage::get_instance()->save_data("parse_ini", null, -86400); 
        } else {
            trace("cfg");
            Config::$_options = $options;
        }
    }
    // execute $fn if option enabled
    public static function if_en(string $option_name, $fn) {
        if (Config::$_options[$option_name]) { $fn(); }
    }

    // set a single value
    public static function set_value(string $option_name, $value) {
        Config::$_options[$option_name] = $value;
    }

    // return true if value is set to true or "block"
    public static function is_block(string $name) : bool {
        $value = self::$_options[$name]??'';
        return ($value === 'block' || $value == true) ? true : false;
    }

    // return true if value is set to "report" or "alert"
    public static function is_report(string $name) : bool {
        $value = self::$_options[$name]??'';
        return ($value === 'report' || $value === 'alert') ? true : false;
    }

    // get a string value with a default
    public static function str(string $name, string $default = '') : string {
        if ($name == "auto_start") { // UGLY HACK for settings.html
            $ini = ini_get("auto_prepend_file");
            $found = false;
            if (!empty($ini)) {
                if ($_SERVER['IS_WPE']??false || CFG::enabled("emulate_wordfence")) {
                    $file = CFG::str("cms_root")."/wordfence-waf.php";
                    if (file_exists($file)) {
                        $s = @stat($file); // cant read this file on WPE, check the size
                        $found = ($s['size']??9999 < 256);
                    }
                }
                else if (contains($ini, "bitfire")) { $found = true; }
            }
            return ($found) ? "on" : "";
        }
        if (isset(Config::$_options[$name])) { return (string) Config::$_options[$name]; }
        return (string) $default;
    }

    public static function str_up(string $name, string $default = '') : string {
        return strtoupper(Config::str($name, $default));
    }

    // get an integer value with a default
    public static function int(string $name, int $default = 0) : int {
        return intval(Config::$_options[$name] ?? $default);
    }

    public static function arr(string $name, array $default = array()) : array {
        return (isset(Config::$_options[$name]) && is_array(Config::$_options[$name])) ? Config::$_options[$name] : $default;
    }

    public static function enabled(string $name, bool $default = false) : bool {
        $value = self::$_options[$name]??$default;
        if ($value === "block" || $value === "report" || $value == true) { return true; }
        return $default;
    }

    public static function disabled(string $name, bool $default = true) : bool {
        return !Config::enabled($name, !$default);
    }

    public static function file(string $name) : string {
        if (!isset(Config::$_options[$name])) { return ''; }
        if (Config::$_options[$name][0] === '/') { return (string)Config::$_options[$name]; }
        return \BitFire\WAF_ROOT . (string)Config::$_options[$name];
    }
}

/**
 * NOT PURE.  depends on: SERVER['PHP_AUTH_PW'], Config['password']
 */
function verify_admin_password() : Effect {

    // ensure that the server configuration is complete...
    if (CFG::disabled("configured")) { \BitFireSVR\bf_activation_effect()->run(); }
    $effect = Effect::new();
    // disable caching for auth pages
    $effect->response_code(200);

    // run the initial password setup if the password is not configured
    if (CFG::str("password") == "configure") {
        return $effect;
    }

    // allow 
    if (CFG::enabled("bitfire_tech_allow") && $_COOKIE['_bitfire_tech']??false) {
        if (authenticate_tech($_COOKIE['_bitfire_tech'])->compare("allow")) {
            return $effect;
        } 
    }

    $raw_pw = $_SERVER["PHP_AUTH_PW"]??'';
    // read any recovery passwords
    $password = CFG::str("password");
    $files = glob(CFG::str("cms_root")."/bitfire.recovery.*");
    foreach ($files as $file) {
        if (filemtime($file) < time() - 3600) {
            unlink($file);
        } else {
            // set the password and unlock the config file
            $password = trim(file_get_contents($file));
            @chmod(WAF_INI, FILE_RW);
        }
    }

    
    // prefer plugin authentication first
    if (function_exists("BitFirePlugin\is_admin") && \BitFirePlugin\is_admin()) {
        return $effect;
    }

    // inspect the cookie wp admin status, we pass auth if wp value is admin(2)
    // TODO: make this a function on the BitFire class
    $cookie = BitFire::get_instance()->cookie;
    if ($cookie != null) {
        if ($cookie->extract("wp")->value("int") == 2) {
            return $effect;
        }
    }
    

    // if we don't have a password, or the password does not match
    // or the password function is disabled
    // create an effect to force authentication and exit
    if (strlen($raw_pw) < 2 ||
        $password == "disabled" ||
        (hash("sha3-256", $raw_pw) !== $password) &&
        (hash("sha3-256", $raw_pw) !== hash("sha3-256", $password))) {

        $effect->header("WWW-Authenticate", 'Basic realm="BitFire", charset="UTF-8"');
        $effect->response_code(401);
        $effect->exit(true);
    }

    return $effect;
}

/**
 * 
 */
class BitFire
{
    // data storage
    protected $_ip_key;

    // request unique id
    public $uid;
    public $inspected = false;
    public static $_exceptions = NULL;
    public static $_reporting = array();
    public static $_blocks = array();
    /** @var \ThreadFin\MaybeStr $cookie */
    public $cookie = NULL;

    public static $_fail_reasons = array();

    public $_request = null;

    /** @var BitFire $_instance */
    protected static $_instance = null;

    /** @var BotFilter $bot_filter */
    public $bot_filter = null;

    /**
     * WAF is a singleton
     * @return BitFire the bitfire singleton;
     */
    public static function get_instance() {
        if (BitFire::$_instance == null) {
            if (empty(ini_get("date.timezone"))) {
                ini_set("date.timezone", "UTC");
            }
            BitFire::$_instance = new BitFire();
        }
        return BitFire::$_instance;
    }

    /**
     * Create a new instance of the BitFire
     */
    protected function __construct() {

        $this->_request = process_request2($_GET, $_POST, $_SERVER, $_COOKIE); // filter out all request data for parsed use

        // handle a common case urls we never care about
        if (in_array($this->_request->path, CFG::arr("urls_not_found"))) {
            http_response_code(404); die();
        }
    }
    
    /**
     * write report data after script execution 
     */
    public function __destruct() {
        if (count(self::$_reporting) > 0) {
            $coded = array_map(function (array $x):array {
                $x['http_code'] = http_response_code();
                $x['request']->cookies = "**redacted**";
                return $x; }, self::$_reporting);
            json_to_file_effect(get_hidden_file("alerts.json"), $coded)->run();
        }
        if (count(self::$_blocks) > 0) {
            $coded = array_map(function (array $x):array { 
                $x['http_code'] = http_response_code();
                $x['request']->cookies = "**redacted**";
                //$x['request']['headers'] = array_filter($x['request']['headers'], 'array_filter');
                return $x; }, self::$_blocks);
            json_to_file_effect(get_hidden_file("blocks.json"), $coded)->run();
        }
    }

    public function __wakeup() {
        trigger_error("POP chaining not allowed", E_USER_ERROR);
    }

    /**
     * handle API calls.
     */
    
    /**
     * append an exception to the list of exceptions
     */
    public function add_exception(Exception $exception) {
        self::$_exceptions[] = $exception;
    }

    /**
     * create a new block, returns a maybe of a block, empty if there is an exception for it
     * TODO: add blocking exception filtering here so code can know if the block was executed
     */
    public static function new_block(int $code, string $parameter, string $value, string $pattern, int $block_time = 0, ?Request $req = null) : MaybeBlock {
        if ($code === FAIL_NOT) { return Maybe::$FALSE; }
        if ($req == null) { trace("DEFREQ"); $req = BitFire::get_instance()->_request; }

        // add the exception to the list of exceptions if we are still in dynamic exception mode
        if (time() < CFG::int('dynamic_exceptions') && $code != 24002 && $code != 24001 && $code != 25001) {
            $req->post = [
                'path' => $req->path,
                'code' => $code,
                'param' => $parameter,
            ];
            require_once \BitFire\WAF_SRC . 'api.php';
            \BitFire\add_api_exception($req)
                ->hide_output()
                ->run();
            return Maybe::$FALSE;
        }
        trace("BL:[$code]");


        $block = new Block($code, $parameter, substr($value, 0, 2048), $pattern, $block_time);
        if (is_report($block)) {
            if (!$block->skip_reporting) {
                self::reporting($block, $req, false);
            }
            trace("RPT[$code]");
            return Maybe::$FALSE;
        }
        self::$_exceptions = (self::$_exceptions === NULL) ? load_exceptions() : self::$_exceptions;
        $filtered_block = filter_block_exceptions($block, self::$_exceptions, $req);

        // do the logging
        if (!$filtered_block->empty()) {
            if (!$block->skip_reporting) {
                self::reporting($filtered_block(), $req, true);
            }
            trace("BLOCK[$code]");
        }
        return $filtered_block;
    }
    
    /**
     * report a block
     * @param bool $report_or_block true if this is a block, false if it is a report
     */
    protected static function reporting(Block $block, \BitFire\Request $request, bool $report_or_block = false) {

        $mt = microtime(true);
        $st = isset($GLOBALS['start_time']) ? $GLOBALS['start_time'] : $mt-0.01;
        $time_diff = $mt - $st;
        $data = array('time' => utc_date('r'), 'tv' => utc_time(),
            'exec' => @number_format($time_diff, 6). ' sec',
            'block' => $block,
            'request' => $request);
        $bf = BitFire::get_instance()->bot_filter;
        if ($bf != null) {
            $data['browser'] = (array) $bf->browser;
            $data['rate'] = $bf->ip_data;
        }
        
        if ($report_or_block) {
            self::$_blocks[] = $data;
        } else {
            self::$_reporting[] = $data;
        }
    }
    


    /**
     * inspect a request and block failed requests
     * return false if inspection failed...
     */
    public function inspect() : MaybeBlock {
        $this->inspected = true;
        trace("ins");
        // HATE TO PUT THIS HERE, BUT WE NEED CFG LOADED SO WE CAN INCLUDE CORRECT PLUGIN
        require_once \BitFire\WAF_SRC."cms.php";

        // make sure that the default empty block is actually empty, hard code here because this data is MUTABLE for performance *sigh*
        Maybe::$FALSE = MaybeBlock::of(NULL);
        $block = MaybeBlock::of(NULL);

        // handle urls that this site does not want to inspect
        if (in_array($this->_request->path, CFG::arr("urls_ignored"))) {
            trace("ign");
            return Maybe::$FALSE;
        }


        // don't inspect local commands, this will skip command line access in case we are running via auto_prepend
        if (!isset($_SERVER['REQUEST_URI'])) { trace("local"); return $block; }

        // block from the htaccess file
        if (isset($this->_request->get['_bf_block'])) {
            trace("htaccess");
            return BitFire::new_block(28001, "_bf_block", "url", $this->_request->get['_bf_block'], 0);
        }

        
        // Do we have a logged in bitfire cookie? don't block.
        $maybe_bot_cookie = decrypt_tracking_cookie(
            $_COOKIE[Config::str(CONFIG_USER_TRACK_COOKIE)] ?? '',
            Config::str(CONFIG_ENCRYPT_KEY),
            $this->_request->ip, $this->_request->agent);
        CFG::set_value("wp", $maybe_bot_cookie->extract("wp", 0)->value('int'));
        $this->cookie = $maybe_bot_cookie;

        //debug("cookie %s", print_r($maybe_bot_cookie, true));

 
        // if we have an api command and not running in WP, execute it. we are done!
        if ((isset($this->_request->get[BITFIRE_COMMAND]) || isset($this->_request->post[BITFIRE_COMMAND])) && !isset($this->_request->get['plugin'])) {
            require_once WAF_SRC."api.php";
            api_call($this->_request)->run();
        }



        // if we are not running inside of Wordpress, then we need to load the page here.
        // if running inside of WordPress, bitfire-admin.php will load the admin pages, so
        // the check for admin.php will fail here in that case
        $no_slash_fn = partial_right('trim', '/');
        $dash_path = contains($no_slash_fn($this->_request->path), ['bitfire/startup.php', $no_slash_fn(CFG::str("dashboard_path"))]);
        if ($dash_path && (
            !isset($this->_request->get['BITFIRE_PAGE']) && !isset($this->_request->get['BITFIRE_API']))) {
            $this->_request->get['BITFIRE_PAGE'] = 'DASHBOARD';
        }

        if (isset($this->_request->get['BITFIRE_PAGE'])) {
            require_once \BitFire\WAF_SRC."dashboard.php";

            $p = strtoupper($this->_request->get['BITFIRE_PAGE']);
            if ($p === "MALWARESCAN") {
                serve_malware();
            }
            else if ($p === "SETTINGS") {
                serve_settings();
            }
            else if ($p === "ADVANCED") {
                serve_advanced();
            }
            else if ($p === "EXCEPTIONS") {
                serve_exceptions();
            }
            else if ($p === "DATABASE") {
                serve_database();
            }
            else if ($p === "BOTLIST") {
                serve_bot_list();
            }
            else {
                serve_dashboard();
            }
            exit;
        }

               
        

        // QUICK BAIL OUT IF DISABLED
        if (!Config::enabled(CONFIG_ENABLED)) { trace("DISABLE"); return $block; }

               
        // TODO: improve this, move browser type to bitfire main class and always identify the browser
        // we will need cache storage and secure cookies
        $this->bot_filter = new BotFilter(CacheStorage::get_instance());
        // bot filtering
        if ($this->bot_filter_enabled()) {
            $block = $this->bot_filter->inspect($this->_request);
        } else {
            // get details about the agent
            $this->bot_filter->browser = \BitFireBot\parse_agent($request->agent);
        }

        // send headers first
        if (Config::enabled(CONFIG_SECURITY_HEADERS) || CFG::enabled("csp_policy_enabled")) {
            require_once \BitFire\WAF_SRC."headers.php";
            \BitFireHeader\send_security_headers($this->_request, $maybe_bot_cookie, $this->bot_filter->browser)->run();
        } else { trace("NODHR"); }

        $wp_admin = ($maybe_bot_cookie->extract("wp")() > 1);

        // build A WordPress Profile for REAL browsers only
        if (CFG::enabled("profiling") && $this->bot_filter->browser->valid > 1) {

            $wp_effect = cms_build_profile($this->_request, $wp_admin);
            register_shutdown_function(function() use ($wp_effect) {

                // if we have wordpress db, and query data
                if (CFG::enabled("audit_sql")) {
                    $tx_log = CFG::str("tx_log");
                    if (strlen($tx_log) > 0) {
                        $wp_effect->file(
                            new FileMod(\BitFire\WAF_ROOT."/cache/sql_tx.log", 
                            CFG::str("tx_log"), FILE_W, 0, true));
                    }
                }
                $wp_effect->run();
                if ($wp_effect->num_errors() > 0) {
                    if (CFG::enabled("debug_file")) {
                        debug("effect errors [%s]", en_json($wp_effect->read_errors()));
                    }
                }
            });
        }



        // always return consistent results for wordpress scanner blocks regardless of bot type
        // we want to fool scanners to think nginx/apache sent this response ...
        if (CFG::enabled("wp_block_scanners") && function_exists("BitFirePRO\block_plugin_enumeration")) {
            \BitfirePRO\block_plugin_enumeration($this->_request)->run();
        }

       
        // generic filtering
        if ($block->empty() && Config::enabled(CONFIG_WEB_FILTER_ENABLED)) {
            require_once \BitFire\WAF_SRC.'webfilter.php';
            $web_filter = new \BitFire\WebFilter();
            $block = $web_filter->inspect($this->_request, $this->cookie);
        }

        // 1% cleanup old cache files
        if (mt_rand(0, 100) < 2) {
            $cache_file_list = glob(WAF_ROOT."cache/objects/*");
            array_walk($cache_file_list, function ($file) {
                $success = false;
                $path = realpath($file);
                if (file_exists($path)) {
                    @include ($path);
                    if (!$success) {
                        @unlink($file);
                    }
                }
            });
        }

        // quick approx stats occasionally
        if (random_int(1, 100) == 81) {
            trace("stat");
            $f = \BitFire\WAF_ROOT."/cache/ip.8.txt";$n=un_json(file_get_contents($f));
            if ($n['t'] < time()) { $n['h']=$this->_request->host; http2("POST", APP."zxf.php", base64_encode(json_encode($n))); $n['v']=BITFIRE_VER;$n['c']=0; $n['t']=time()+DAY; unset($n['host']); }
            $n['c']++;file_put_contents($f, en_json($n), LOCK_EX);
        }

        return $block;
    }

    /**
     * @return bool true if any bot blocking features are enabled
     */
    protected function bot_filter_enabled() : bool {
        // disable bot filtering for internal requests
        $bf = $this->_request->get[BITFIRE_INPUT] ?? '';
        if ($bf === trim(Config::str(CONFIG_SECRET, 'bitfiresekret'))) { return false; }

        return (
            Config::enabled(CONFIG_CHECK_DOMAIN) ||
            Config::enabled(CONFIG_BLACKLIST_ENABLE) ||
            Config::enabled(CONFIG_WHITELIST_ENABLE) ||
            Config::enabled(CONFIG_REQUIRE_BROWSER) ||
            Config::enabled(CONFIG_HONEYPOT) ||
            Config::str(CONFIG_RATE_LIMIT_ACTION) !== '');
    }
}

/**
 * called to handle some internal setup
 * @return void 
 */
function bitfire_init() {
    if (strlen(CFG::str('pro_key')) > 20) {
        if (file_exists(\BitFire\WAF_SRC . 'pro.php')) {
            @include_once \BitFire\WAF_SRC . 'pro.php';
        }
    }
}

/**
 * create  an effect that will render the block page
 * @param int $code the unique code for this line of code
 * @param string $parameter the parameter name where the issue was detected
 * @param string $value  the value of the detected parameter
 * @param string $pattern  the pattern that was matched
 * @param int $block_time one of BLOCK_SHORT, BLOCK_MEDIUM, BLOCK_LONG
 * @param null|Request $req the offending request
 * @return Effect 
 */
function block_now(int $code, string $parameter, string $value, string $pattern, int $block_time = 0, ?Request $req = null, ?string $custom_err = null) : Effect {
    if ($req == null) { $req = BitFire::get_instance()->_request; }

    $block = BitFire::new_block($code, $parameter, $value, $pattern, $block_time, $req);
    if (!$block->empty()) {
        $block = $block();
        $uuid = $block->uuid;
        $error_css = get_public("error.css");
        $block_type = htmlentities($block->__toString());
        ob_start();
        if (empty($custom_err)) { $custom_err = "This site is protected by BitFire RASP. <br> Your action: <strong> $block_type</strong> was blocked."; }  
        require WAF_ROOT."views/block.php";
        $effect = Effect::new()
            ->status(403)
            ->out(ob_get_clean())
            ->http("POST", APP."blocks.php", (string)json_encode(make_log_data($req, $block, NULL)), array("Content-Type" => "application/json"))
            ->exit(true);

        return $effect;

    }
    return Effect::new();
}