yf_csrf_guard class

CSRF protection.

Fileclasses/yf_csrf_guard.class.php
Use_class('csrf_guard')

Public properties

A site sets them in its config with $PROJECT_CONF['csrf_guard']['NAME'] = ...;

PropertyDefaultDescription
$ENABLED true
$ENABLED_FOR ['admin' => false, 'user' => true]
$FORM_ID null
$HASH_ALGO 'sha256'
$HEADER_NAME
string
'HTTP_X_CSRF_TOKEN' Server key of the request header carrying the token (AJAX)
$LOG_DIR_NAME 'logs/'
$LOG_DRIVER null
$LOG_ERRORS true
$SESSION_TOKEN_KEY
string
'__session' Key of the per-session token inside the session token array, used by check() for non-form2 requests
$TOKEN_NAME '_token'
$_chained_mode false
$_extend []
$is []

Public methods 14

check

public function check($value = null, $main_type = null)

Non-consuming check for the dispatcher: the token from POST or the request header must match the session token or one of the pending form2 tokens (form2 still validates and consumes its own token afterwards).

  • $value null|string
  • $main_type null|string
    public function check($value = null, $main_type = null)
    {
        if ( ! $this->is_enabled($main_type)) {
            return true;
        }
        // a stale form2 token in POST must not hide a valid header, so both sources are tried
        $values = $value === null ? [$_POST[$this->TOKEN_NAME] ?? '', $_SERVER[$this->HEADER_NAME] ?? ''] : [$value];
        $tokens = (array) ($_SESSION[$this->TOKEN_NAME] ?? []);
        foreach ($values as $candidate) {
            if ( ! is_string($candidate) || $candidate === '') {
                continue;
            }
            foreach ($tokens as $token) {
                if (is_string($token) && $token !== '' && hash_equals($token, $candidate)) {
                    return true;
                }
            }
        }
        return false;
    }

configure

public function configure($params = [])
    public function configure($params = [])
    {
        if (isset($params['form_id'])) {
            $this->FORM_ID = $params['form_id'];
        }
        if (isset($params['token_name'])) {
            $this->TOKEN_NAME = $params['token_name'];
        }
        if (isset($params['hash_algo'])) {
            $this->HASH_ALGO = $params['hash_algo'];
        }
        return $this;
    }

del

public function del()
    public function del()
    {
        $key = $this->FORM_ID;
        $_SESSION[$this->TOKEN_NAME][$key] = '';
        unset($_SESSION[$this->TOKEN_NAME][$key]);
        return $this;
    }

generate

public function generate()
    public function generate()
    {
        $token = bin2hex(random_bytes(32));
        $this->set($token);
        return $token;
    }

get

public function get()
    public function get()
    {
        $key = $this->FORM_ID;
        if (isset($_SESSION[$this->TOKEN_NAME][$key])) {
            return $_SESSION[$this->TOKEN_NAME][$key] ?? null;
        }
        return false;
    }

is_enabled

public function is_enabled($main_type = null)
  • $main_type null|string
    public function is_enabled($main_type = null)
    {
        $main_type = $main_type ?: MAIN_TYPE;
        return (bool) $this->ENABLED && ! empty($this->ENABLED_FOR[$main_type]);
    }

log_error

public function log_error($params = [])
    public function log_error($params = [])
    {
        if ( ! $this->LOG_ERRORS) {
            return false;
        }
        $main = main();
        // slice 2 first elements (__FUNCTION__ and $this->_go) and leave only 5 more trace elements to save space
        $trace = implode(PHP_EOL, array_slice(explode(PHP_EOL, $main->trace_string()), 2, 7));
        $is_https = $main->is_https();
        return db()->insert_safe('log_csrf_errors', [
            'form_id' => $params['form_id'] ?? '',

            'date' => date('Y-m-d H:i:s'),
            'ip' => common()->get_ip(),
            'url' => ($is_https ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? '') . ($_SERVER['REQUEST_URI'] ?? '') . (($_SERVER['QUERY_STRING'] ?? '') ? '?' . $_SERVER['QUERY_STRING'] : ''),
            'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
            'referer' => $_SERVER['HTTP_REFERER'] ?? '',
            'object' => $_GET['object'] ?? '',
            'action' => $_GET['action'] ?? '',
            'user_id' => MAIN_TYPE_ADMIN ? $main->ADMIN_ID : $main->USER_ID,
            'user_group' => MAIN_TYPE_ADMIN ? $main->ADMIN_GROUP : $main->USER_GROUP,
            'site_id' => (int) $main->SITE_ID,
            'server_id' => (int) $main->SERVER_ID,

            'is_logged_in' => (bool) $main->is_logged_in(),
            'is_common_page' => $main->is_common_page(),
            'is_https' => $main->is_https(),
            'is_post' => $main->is_post(),
            'is_no_graphics' => (bool) $main->no_graphics(),
            'is_ajax' => $main->is_ajax(),
            'is_spider' => $main->is_spider(),
            'is_redirect' => $main->is_redirect(),
            'is_console' => $main->is_console(),
            'is_unit_test' => $main->is_unit_test(),
            'is_dev' => $main->is_dev(),
            'is_debug' => $main->is_debug(),
            'is_banned' => $main->is_banned(),
            'is_403' => $main->is_403(),
            'is_404' => $main->is_404(),
            'is_503' => $main->is_503(),
            'is_cache_on' => $main->is_cache_on(),
            'is_mobile' => $main->is_mobile(),

            'lang' => (string) conf('language'),
            'country' => (string) (conf('country') ?: ($_SERVER['GEOIP_COUNTRY_CODE'] ?? '')),
            'utm_source' => (string) (($_GET['utm_source'] ?? '') ?: (($_POST['utm_source'] ?? '') ?: ($_SESSION['utm_source'] ?? ''))),
            'post_data' => json_encode($this->_redact($_POST)),
            'trace' => $trace,
            'memory' => (int) memory_get_peak_usage(),
            'exec_time' => str_replace(',', '.', round(microtime(true) - $main->_time_start, 4)),
            'num_dbq' => (int) db()->NUM_QUERIES,
            'page_size' => (int) tpl()->_output_body_length,
        ]);
    }

session_token

public function session_token()

Per-session token for requests that are not form2 forms (AJAX, confirm pages); created on first use.

    public function session_token()
    {
        $key = $this->SESSION_TOKEN_KEY;
        if (empty($_SESSION[$this->TOKEN_NAME][$key]) || ! is_string($_SESSION[$this->TOKEN_NAME][$key])) {
            $_SESSION[$this->TOKEN_NAME][$key] = bin2hex(random_bytes(32));
        }
        return $_SESSION[$this->TOKEN_NAME][$key];
    }

set

public function set($value)
    public function set($value)
    {
        $key = $this->FORM_ID;
        $_SESSION[$this->TOKEN_NAME][$key] = $value;
        return $this;
    }

validate

public function validate($token_value)
    public function validate($token_value)
    {
        if ( ! $this->ENABLED || ! $this->ENABLED_FOR[MAIN_TYPE]) {
            return true;
        }
        $token = $this->get();
        if ($token === false) {
            $result = false;
        } elseif ($token === $token_value) {
            $result = true;
        } else {
            $result = false;
        }
        $this->del();
        return $result;
    }

Methods starting with _ hooks and helpers; not routable as page actions

__call

public function __call($name, $args)

Catch missing method call.

    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args, $this->_chained_mode);
    }

_extend

public function _extend($name, $func)
    public function _extend($name, $func)
    {
        $this->_extend[$name] = $func;
    }

_redact

public function _redact($data)

Passwords and tokens never reach the log table.

    public function _redact($data)
    {
        foreach ((array) $data as $k => $v) {
            if (is_array($v)) {
                $data[$k] = $this->_redact($v);
            } elseif (preg_match('~pass|pswd|token|secret~i', (string) $k)) {
                $data[$k] = '***';
            }
        }
        return $data;
    }