yf_csrf_guard classCSRF protection.
| File | classes/yf_csrf_guard.class.php |
|---|---|
| Use | _class('csrf_guard') |
A site sets them in its config with $PROJECT_CONF['csrf_guard']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$COOKIE_NAMEstring |
'yf_csrf' |
Readable cookie that hands the session token to the admin JavaScript |
$COOKIE_SAMESITEstring |
'Lax' |
|
$ENABLED |
true |
|
$ENABLED_FOR |
['admin' => false, 'user' => true] |
|
$FORM_ID |
null |
|
$HASH_ALGO |
'sha256' |
|
$HEADER_NAMEstring |
'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_KEYstring |
'__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 |
|
$_cookie_sentbool |
false |
@conf_skip |
$_extend |
[] |
|
$is |
[] |
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;
}
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;
}
public function del()
public function del()
{
$key = $this->FORM_ID;
$_SESSION[$this->TOKEN_NAME][$key] = '';
unset($_SESSION[$this->TOKEN_NAME][$key]);
return $this;
}
public function generate()
public function generate()
{
$token = bin2hex(random_bytes(32));
$this->set($token);
return $token;
}
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;
}
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]);
}
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,
]);
}
public function send_cookie()
Hand the session token to the browser once per request, for the X-CSRF-Token header set by yf_js_admin.
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];
}
public function set($value)
public function set($value)
{
$key = $this->FORM_ID;
$_SESSION[$this->TOKEN_NAME][$key] = $value;
return $this;
}
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;
}
_ hooks and helpers; not routable as page actionspublic function __call($name, $args)
Catch missing method call.
public function __call($name, $args)
{
return main()->extend_call($this, $name, $args, $this->_chained_mode);
}
public function _extend($name, $func)
public function _extend($name, $func)
{
$this->_extend[$name] = $func;
}
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;
}