<?php
namespace Phad;
trait SpamControl {
/** either POST or GET, depending which request method was used */
public $spam_data = [];
/** I don't know. It's used for the csrf spam controls */
public $latest_csrf = [];
/** I don't know. It's used for the csrf spam controls */
public $valid_sessions = [];
/**
* Prepare CSRF & Honeypot spam controls & return html inputs for them
*
* @param $key a key to identify the form for csrf validation
*/
public function show_spam_control(string $key){
$csrf_key = $this->enable_csrf($key, 60, parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
$output = $this->get_csrf_session_input($key)
. $this->honey_form();
// $output = $this->honey_form();
return $output;
}
public function verify_spam_control(string $key, array &$row){
$this->spam_data = $row;
$honey = $this->check_honey();
$csrf = $this->csrf_is_valid($key);
$csrf_key = $this->get_csrf_post_key($key);
$csrf = true;
$fail = false;
if (!$honey
||!$csrf
) {
$fail = true;
$url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
echo "<h1>Submission Error!</h1>";
echo "<p>Our spam control system blocked your submission. Please <a href=\"$url\">Try Again</a> or contact us on Social Media if this problem continues.</p>";
}
// remove honey keys fro the post
$honey_keys = explode(',',$row['honey']);
foreach ($honey_keys as $hkey){
unset($row[$hkey]);
}
unset($row['honey']);
unset($row['honey_answer']);
unset($row[$csrf_key]);
if ($fail){
$row['spam_control'] = 'failed_validation';
return false;
}
return true;
}
public function honey_form(){
ob_start();
require(__DIR__.'/honey_form.php');
return ob_get_clean();
}
protected function check_honey(){
if (!isset($this->spam_data['honey']))return false;
$honey = $this->spam_data['honey'];
$names = explode(',', $honey);
if (count($names)!=3)return false;
if (!isset($this->spam_data[$names[0]])
||$this->spam_data[$names[0]]!==''
)return false;
if (!isset($this->spam_data[$names[1]])
||$this->spam_data[$names[1]]!==''
)return false;
if (!isset($this->spam_data[$names[2]])
||$this->spam_data[$names[2]]==''
)return false;
if (!isset($this->spam_data['honey_answer'])
||$this->spam_data['honey_answer'] == ''
)return false;
// echo 'no';
// exit;
// var_dump($this->spam_data['honey_answer']);
// var_dump($this->spam_data[$names[2]]);
// var_dump(password_hash($this->spam_data[$names[2]], PASSWORD_DEFAULT));
// if (!password_verify($this->spam_data[$names[2]], $this->spam_data['honey_answer']))return false;
if (md5($this->spam_data[$names[2]]) != $this->spam_data['honey_answer'])return false;
return true;
}
//////////////////////
// throttle
//////////////////////
public function throttle($key, $value, $length=5000){
$lib = $this->lib;
$remaining = $lib->throttle_remaining($key,$value);
if ($remaining>0){
$seconds = (int)(($remaining + 1000) / 1000);
echo "<p class=\"error\">Please wait $seconds seconds before trying again.</p>";
error_log("Throttle for '$key' with value '$value' has '$seconds' remaining");
return new \Tlf\User\BlackHole($this);
}
$lib->add_throttle($key, $value, $length);
return $this;
}
////////////
// csrf
////////////
//
public function make_csrf_code(){
// this code from symfony csrf package: https://github.com/symfony/security-csrf/blob/5.4/TokenGenerator/UriSafeTokenGenerator.php
$bytes = random_bytes(64);
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}
/**
*
*
* @param $key_prefix string to help identify your csrf token.
* @param $expiry_minutes number of minutes the token should be valid for
* @param $url the url path the token should be validated on, like '/some/url/'. If not set, it works on any path
*
* @return the csrf key. To load csrf data do `$_SESSION[$csrf_key]`. `$csrf_key` will be like `key_prefix-csrf-uniqid()`
*/
public function enable_csrf(string $key_prefix='',int $expiry_minutes=60, string $url_path=''){
$key = $key_prefix.'-csrf-'.uniqid();
$data = [
'code'=> $this->make_csrf_code(),
'expires_at' => time() + $expiry_minutes * 60,
'uri' => $url_path,
];
if (session_status()==PHP_SESSION_NONE)session_start();
if (session_status()!=PHP_SESSION_ACTIVE)throw new \Exception("Failed to start session. Cannot do csrf without session.");
$_SESSION[$key] = $data;
$this->latest_csrf[$key_prefix] = $key;
// error_log('csrf key: '.$key);
return $key;
}
/**
* get the key of the csrf data in `$_POST` for the given key
* @param $key_prefix see csrf_is_valid
*/
public function get_csrf_post_key(string $key_prefix=''): string {
$len = strlen($key_prefix) + strlen('-csrf-');
foreach ($_POST as $key=>$value){
if (substr($key,0,$len)!=$key_prefix.'-csrf-')continue;
$post_key = $key;
// $post_code = $value;
return $post_key;
// break;
}
return '';
}
public function get_csrf_session_key(string $key_prefix=''): string {
if (isset($this->latest_csrf[$key_prefix]))return $this->latest_csrf[$key_prefix];
$len = strlen($key_prefix) + strlen('-csrf-');
foreach ($_SESSION as $key=>$value){
if (substr($key,0,$len)!=$key_prefix.'-csrf-')continue;
return $key;
}
return '';
}
public function get_csrf_session_input(string $key_prefix=''): string {
$key = $this->get_csrf_session_key($key_prefix);
$code = $_SESSION[$key]['code'];
return '<input type="hidden" name="'.$key.'" value="'.$code.'">';
}
/**
* Checks `$_POST` for the csrf token
*
* @param $key_prefix the same key prefix you passed to `$this->enable_csrf()`
* @return true/false
*/
public function csrf_is_valid(string $key_prefix=''): bool {
// this attempts to do the checks listed on https://www.taeluf.com/blog/php/security/csrf-validation/
$post_key = $this->get_csrf_post_key($key_prefix);
if ($post_key=='')return false;
$post_code = $_POST[$post_key];
// because i unset from $_SESSION
if (isset($this->valid_sessions[$post_key]))return true;
if (session_status()==PHP_SESSION_NONE)session_start();
if (session_status()!=PHP_SESSION_ACTIVE)throw new \Exception("Failed to start session. Cannot do csrf without session.");
if (!isset($_SESSION[$post_key]))return false;
$session_csrf = $_SESSION[$post_key];
if ($session_csrf['code'] != $post_code) return false;
if ($session_csrf['expires_at'] < time()) return false;
$post_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($session_csrf['uri'] != ''
&&$session_csrf['uri'] != $post_path
)return false;
if (!isset($_SERVER['HTTP_REFERER']))return false;
$referer_domain = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
// to remove the port (mainly bc of localhost testing)
$server_host = parse_url($_SERVER['HTTP_HOST'], PHP_URL_HOST);
if ($referer_domain != $server_host)return false;
unset($_SESSION[$post_key]);
$this->valid_sessions[$post_key] = true;
return true;
}
}