CSRF from Hanyuu.

This commit is contained in:
flash 2019-12-11 19:10:54 +01:00
parent 2728a1fca9
commit 51f3c47a31
30 changed files with 151 additions and 169 deletions

View file

@ -39,7 +39,6 @@ require_once 'src/audit_log.php';
require_once 'src/changelog.php';
require_once 'src/colour.php';
require_once 'src/comments.php';
require_once 'src/csrf.php';
require_once 'src/manage.php';
require_once 'src/news.php';
require_once 'src/perms.php';
@ -456,10 +455,8 @@ MIG;
}
}
csrf_settings(
Config::get('csrf.secret', Config::TYPE_STR, 'insecure'),
empty($userDisplayInfo) ? ip_remote_address() : $cookieData['session_token']
);
CSRF::setGlobalSecretKey(Config::get('csrf.secret', Config::TYPE_STR, 'soup'));
CSRF::setGlobalIdentity(empty($userDisplayInfo) ? ip_remote_address() : $cookieData['session_token']);
if(Config::get('private.enabled', Config::TYPE_BOOL)) {
$onLoginPage = $_SERVER['PHP_SELF'] === url('auth-login');

View file

@ -24,7 +24,7 @@ $ipAddress = ip_remote_address();
$remainingAttempts = user_login_attempts_remaining($ipAddress);
while(!empty($_POST['login']) && is_array($_POST['login'])) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Was unable to verify the request, please try again!';
break;
}

View file

@ -8,7 +8,7 @@ if(!user_session_active()) {
return;
}
if(csrf_verify_request()) {
if(CSRF::validateRequest()) {
setcookie('msz_auth', '', -9001, '/', '', !empty($_SERVER['HTTPS']), true);
user_session_stop(true);
url_redirect('index');

View file

@ -30,7 +30,7 @@ $remainingAttempts = user_login_attempts_remaining($ipAddress);
while($canResetPassword) {
if(!empty($reset) && $userId > 0) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Was unable to verify the request, please try again!';
break;
}
@ -73,7 +73,7 @@ while($canResetPassword) {
}
if(!empty($forgot)) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Was unable to verify the request, please try again!';
break;
}

View file

@ -18,7 +18,7 @@ $restricted = ip_blacklist_check(ip_remote_address()) ? 'blacklist'
: (user_warning_check_ip(ip_remote_address()) ? 'ban' : '');
while(!$restricted && !empty($register)) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Was unable to verify the request, please try again!';
break;
}

View file

@ -26,7 +26,7 @@ if(empty($tokenInfo['user_totp_key'])) {
}
while(!empty($twofactor)) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Was unable to verify the request, please try again!';
break;
}

View file

@ -15,7 +15,7 @@ if($isXHR) {
return;
}
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
echo render_info_or_json($isXHR, "Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
@ -36,7 +36,7 @@ if(user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) {
return;
}
csrf_http_header();
header(CSRF::header());
$commentPerms = comments_get_perms($currentUserId);
$commentId = !empty($_GET['c']) && is_string($_GET['c']) ? (int)$_GET['c'] : 0;

View file

@ -10,7 +10,7 @@ switch($indexMode) {
case 'mark':
$markEntireForum = $forumId === 0;
if(user_session_active() && csrf_verify_request()) {
if(user_session_active() && CSRF::validateRequest()) {
forum_mark_read($markEntireForum ? null : $forumId, user_session_current('user_id', 0));
}

View file

@ -13,7 +13,7 @@ if($isXHR) {
return;
}
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
echo render_info_or_json($isXHR, "Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
@ -34,7 +34,7 @@ if(user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) {
return;
}
csrf_http_header();
header(CSRF::header());
if(empty($_POST['poll']['id']) || !ctype_digit($_POST['poll']['id'])) {
echo render_info_or_json($isXHR, "Invalid request.", 400);

View file

@ -19,7 +19,7 @@ if($isXHR) {
return;
}
$postRequestVerified = csrf_verify_request();
$postRequestVerified = CSRF::validateRequest();
if(!empty($postMode) && !user_session_active()) {
echo render_info_or_json($isXHR, 'You must be logged in to manage posts.', 401);
@ -47,7 +47,7 @@ if($isXHR) {
return;
}
csrf_http_header();
header(CSRF::header());
}
$postInfo = forum_post_get($postId, true);

View file

@ -133,7 +133,7 @@ if(!empty($_POST)) {
$topicType = isset($_POST['post']['type']) ? (int)$_POST['post']['type'] : null;
$postSignature = isset($_POST['post']['signature']);
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Could not verify request.';
} else {
$isEditingTopic = empty($topic) || ($mode === 'edit' && $post['is_opening_post']);

View file

@ -82,12 +82,12 @@ if(in_array($moderationMode, $validModerationModes, true)) {
return;
}
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
echo render_info_or_json($isXHR, "Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
csrf_http_header();
header(CSRF::header());
if(!user_session_active()) {
echo render_info_or_json($isXHR, 'You must be logged in to manage posts.', 401);

View file

@ -10,7 +10,7 @@ if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_P
$changeId = (int)($_GET['c'] ?? 0);
if($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_verify_request()) {
if($_SERVER['REQUEST_METHOD'] === 'POST' && CSRF::validateRequest()) {
if(!empty($_POST['change']) && is_array($_POST['change'])) {
if($changeId > 0) {
$postChange = DB::prepare('

View file

@ -10,7 +10,7 @@ if(!perms_check_user(MSZ_PERMS_CHANGELOG, user_session_current('user_id'), MSZ_P
$tagId = (int)($_GET['t'] ?? 0);
if(!empty($_POST['tag']) && is_array($_POST['tag']) && csrf_verify_request()) {
if(!empty($_POST['tag']) && is_array($_POST['tag']) && CSRF::validateRequest()) {
if($tagId > 0) {
$updateTag = DB::prepare('
UPDATE `msz_changelog_tags`

View file

@ -11,10 +11,10 @@ if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General
$notices = [];
if(!empty($_POST)) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = 'Verification failed.';
} else {
csrf_http_header();
header(CSRF::header());
if(!empty($_POST['blacklist']['remove']) && is_array($_POST['blacklist']['remove'])) {
foreach($_POST['blacklist']['remove'] as $cidr) {

View file

@ -12,7 +12,7 @@ $emoteId = !empty($_GET['e']) && is_string($_GET['e']) ? (int)$_GET['e'] : 0;
$isNew = $emoteId <= 0;
$emoteInfo = !$isNew ? Emoticon::byId($emoteId) : new Emoticon;
if(csrf_verify_request() && isset($_POST['emote_order']) && isset($_POST['emote_hierarchy']) && !empty($_POST['emote_url']) && !empty($_POST['emote_strings'])) {
if(CSRF::validateRequest() && isset($_POST['emote_order']) && isset($_POST['emote_hierarchy']) && !empty($_POST['emote_url']) && !empty($_POST['emote_strings'])) {
$emoteInfo->setUrl($_POST['emote_url'])
->setHierarchy($_POST['emote_hierarchy'])
->setOrder($_POST['emote_order'])

View file

@ -8,7 +8,7 @@ if(!perms_check_user(MSZ_PERMS_GENERAL, user_session_current('user_id'), General
return;
}
if(csrf_verify_request() && !empty($_GET['emote']) && is_string($_GET['emote'])) {
if(CSRF::validateRequest() && !empty($_GET['emote']) && is_string($_GET['emote'])) {
$emoteId = (int)$_GET['emote'];
$emoteInfo = Emoticon::byId($emoteId);

View file

@ -11,7 +11,7 @@ if(!perms_check_user(MSZ_PERMS_NEWS, user_session_current('user_id'), MSZ_PERM_N
$category = [];
$categoryId = (int)($_GET['c'] ?? null);
if(!empty($_POST['category']) && csrf_verify_request()) {
if(!empty($_POST['category']) && CSRF::validateRequest()) {
$originalCategoryId = (int)($_POST['category']['id'] ?? null);
$categoryId = news_category_create(
$_POST['category']['name'] ?? null,

View file

@ -12,7 +12,7 @@ $post = [];
$postId = (int)($_GET['p'] ?? null);
$categories = news_categories_get(0, 0, false, false, true);
if(!empty($_POST['post']) && csrf_verify_request()) {
if(!empty($_POST['post']) && CSRF::validateRequest()) {
$originalPostId = (int)($_POST['post']['id'] ?? null);
$currentUserId = user_session_current('user_id');
$title = $_POST['post']['title'] ?? null;

View file

@ -20,7 +20,7 @@ if($canEditPerms) {
$permissions = manage_perms_list(perms_get_role_raw($roleId ?? 0));
}
if(!empty($_POST['role']) && is_array($_POST['role']) && csrf_verify_request()) {
if(!empty($_POST['role']) && is_array($_POST['role']) && CSRF::validateRequest()) {
$roleHierarchy = (int)($_POST['role']['hierarchy'] ?? -1);
if(!user_check_super($currentUserId) && ($roleId === null

View file

@ -22,7 +22,7 @@ $canEdit = $isSuperUser || user_check_authority($currentUserId, $userId);
$canEditPerms = $canEdit && perms_check_user(MSZ_PERMS_USER, $currentUserId, MSZ_PERM_USER_MANAGE_PERMS);
$permissions = manage_perms_list(perms_get_user_raw($userId));
if(csrf_verify_request() && $canEdit) {
if(CSRF::validateRequest() && $canEdit) {
if(!empty($_POST['roles']) && is_array($_POST['roles']) && array_test($_POST['roles'], 'ctype_digit')) {
// Fetch existing roles
$existingRoles = DB::prepare('

View file

@ -62,7 +62,7 @@ if($isEditing) {
]);
if(!empty($_POST) && is_array($_POST)) {
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
$notices[] = MSZ_TMP_USER_ERROR_STRINGS['csrf'];
} else {
if(!empty($_POST['profile']) && is_array($_POST['profile'])) {

View file

@ -15,12 +15,12 @@ if($isXHR) {
return;
}
if(!csrf_verify_request()) {
if(!CSRF::validateRequest()) {
echo render_info_or_json($isXHR, "Couldn't verify this request, please refresh the page and try again.", 403);
return;
}
csrf_http_header();
header(CSRF::header());
if(!user_session_active()) {
echo render_info_or_json($isXHR, 'You must be logged in to manage relations.', 401);

View file

@ -16,7 +16,7 @@ $currentUserId = user_session_current('user_id');
$currentEmail = user_email_get($currentUserId);
$isRestricted = user_warning_check_restriction($currentUserId);
$twoFactorInfo = user_totp_info($currentUserId);
$isVerifiedRequest = csrf_verify_request();
$isVerifiedRequest = CSRF::validateRequest();
if(!$isRestricted && $isVerifiedRequest && !empty($_POST['role'])) {
$roleId = (int)($_POST['role']['id'] ?? 0);

View file

@ -12,7 +12,7 @@ $errors = [];
$currentUserId = user_session_current('user_id');
$sessionActive = user_session_current('session_id');
if(!empty($_POST['session']) && csrf_verify_request()) {
if(!empty($_POST['session']) && CSRF::validateRequest()) {
$currentSessionKilled = false;
if(is_array($_POST['session'])) {

112
src/CSRF.php Normal file
View file

@ -0,0 +1,112 @@
<?php
namespace Misuzu;
use Exception;
use InvalidArgumentException;
final class CSRF {
public const TOLERANCE = 30 * 60;
public const HASH_ALGO = 'sha1';
public const EPOCH = 1575158400;
private $timestamp = 0;
private $tolerance = 0;
private static $globalIdentity = '';
private static $globalSecretKey = '';
public function __construct(int $tolerance = self::TOLERANCE, ?int $timestamp = null) {
$this->setTolerance($tolerance);
$this->setTimestamp($timestamp ?? self::timestamp());
}
public static function timestamp(): int {
return time() - self::EPOCH;
}
public static function setGlobalIdentity(string $identity): void {
self::$globalIdentity = $identity;
}
public static function setGlobalSecretKey(string $secretKey): void {
self::$globalSecretKey = $secretKey;
}
public static function validate(string $token, ?string $identity = null, ?string $secretKey = null): bool {
try {
return self::decode($token, $identity ?? self::$globalIdentity, $secretKey ?? self::$globalSecretKey)->isValid();
} catch(Exception $ex) {
return false;
}
}
public static function token(?string $identity = null, int $tolerance = self::TOLERANCE, ?string $secretKey = null, ?int $timestamp = null): string {
return (new static($tolerance, $timestamp))->encode($identity ?? self::$globalIdentity, $secretKey ?? self::$globalSecretKey);
}
// Should be replaced by filters eventually <
public static function header(...$args): string {
return 'X-Misuzu-CSRF: ' . self::token(...$args);
}
public static function validateRequest(?string $identity = null, ?string $secretKey = null): bool {
if(isset($_SERVER['HTTP_X_MISUZU_CSRF'])) {
$token = $_SERVER['HTTP_X_MISUZU_CSRF'];
} elseif(isset($_POST['_csrf']) && is_string($_POST['_csrf'])) {
$token = $_POST['_csrf'];
} elseif(isset($_REQUEST['csrf']) && is_string($_REQUEST['csrf'])) {
$token = $_REQUEST['csrf'];
} else {
return false;
}
return self::validate($token, $identity, $secretKey);
}
// >
public static function decode(string $token, string $identity, string $secretKey): CSRF {
$hash = substr($token, 12);
$unpacked = unpack('Vtimestamp/vtolerance', hex2bin(substr($token, 0, 12)));
if(empty($hash) || empty($unpacked['timestamp']) || empty($unpacked['tolerance']))
throw new InvalidArgumentException('Invalid token provided.');
$csrf = new static($unpacked['tolerance'], $unpacked['timestamp']);
if(!hash_equals($csrf->getHash($identity, $secretKey), $hash))
throw new InvalidArgumentException('Modified token.');
return $csrf;
}
public function encode(string $identity, string $secretKey): string {
$token = bin2hex(pack('Vv', $this->getTimestamp(), $this->getTolerance()));
$token .= $this->getHash($identity, $secretKey);
return $token;
}
public function getHash(string $identity, string $secretKey): string {
return hash_hmac(self::HASH_ALGO, "{$identity}|{$this->getTimestamp()}|{$this->getTolerance()}", $secretKey);
}
public function getTimestamp(): int {
return $this->timestamp;
}
public function setTimestamp(int $timestamp): self {
if($timestamp < 0 || $timestamp > 0xFFFFFFFF)
throw new InvalidArgumentException('Timestamp must be within the constaints of an unsigned 32-bit integer.');
$this->timestamp = $timestamp;
return $this;
}
public function getTolerance(): int {
return $this->tolerance;
}
public function setTolerance(int $tolerance): self {
if($tolerance < 0 || $tolerance > 0xFFFF)
throw new InvalidArgumentException('Tolerance must be within the constaints of an unsigned 16-bit integer.');
$this->tolerance = $tolerance;
return $this;
}
public function isValid(): bool {
$currentTime = self::timestamp();
return $currentTime >= $this->getTimestamp() && $currentTime <= $this->getTimestamp() + $this->getTolerance();
}
}

View file

@ -32,8 +32,6 @@ final class TwigMisuzu extends Twig_Extension {
public function getFunctions() {
return [
new Twig_Function('get_browser', 'get_browser'),
new Twig_Function('csrf_token', 'csrf_token'),
new Twig_Function('csrf_input', 'csrf_html'),
new Twig_Function('url_construct', 'url_construct'),
new Twig_Function('warning_has_duration', 'user_warning_has_duration'),
new Twig_Function('url', 'url'),
@ -44,6 +42,7 @@ final class TwigMisuzu extends Twig_Extension {
new Twig_Function('forum_may_have_children', 'forum_may_have_children'),
new Twig_Function('forum_may_have_topics', 'forum_may_have_topics'),
new Twig_Function('forum_has_priority_voting', 'forum_has_priority_voting'),
new Twig_Function('csrf_token', fn() => CSRF::token()),
new Twig_Function('git_commit_hash', fn(bool $long = false) => GitInfo::hash($long)),
new Twig_Function('git_tag', fn() => GitInfo::tag()),
new Twig_Function('git_branch', fn() => GitInfo::branch()),

View file

@ -1,127 +0,0 @@
<?php
define('MSZ_CSRF_TOLERANCE', 30 * 60); // DO NOT EXCEED 16-BIT INTEGER SIZES, SHIT _WILL_ BREAK
define('MSZ_CSRF_HTML', '<input type="hidden" name="csrf" value="%1$s">');
define('MSZ_CSRF_HASH_ALGO', 'sha256');
define('MSZ_CSRF_TOKEN_LENGTH', 76); // 8 + 4 + 64
// the following three functions DO NOT depend on csrf_init().
// $identity = When the user is logged in I recommend just using their session key, otherwise IP will be fine.
function csrf_token_create(
string $identity,
string $secretKey,
?int $timestamp = null,
int $tolerance = MSZ_CSRF_TOLERANCE
): string {
$timestamp = $timestamp ?? time();
$token = bin2hex(pack('Vv', $timestamp, $tolerance));
return $token . csrf_token_hash(
MSZ_CSRF_HASH_ALGO,
$identity,
$secretKey,
$timestamp,
$tolerance
);
}
function csrf_token_hash(
string $algo,
string $identity,
string $secretKey,
int $timestamp,
int $tolerance
): string {
return hash_hmac(
$algo,
implode(',', [$identity, $timestamp, $tolerance]),
$secretKey
);
}
function csrf_token_verify(
string $token,
string $identity,
string $secretKey
): bool {
if(empty($token) || strlen($token) !== MSZ_CSRF_TOKEN_LENGTH) {
return false;
}
[$timestamp, $tolerance] = [0, 0];
extract(unpack('Vtimestamp/vtolerance', hex2bin(substr($token, 0, 12))));
if(time() > $timestamp + $tolerance) {
return false;
}
// remove timestamp + tolerance from token
$token = substr($token, 12);
$compare = csrf_token_hash(
MSZ_CSRF_HASH_ALGO,
$identity,
$secretKey,
$timestamp,
$tolerance
);
return hash_equals($compare, $token);
}
// Sets some defaults
function csrf_settings(?string $secretKey = null, ?string $identity = null): array {
static $settings = [];
if(!empty($secretKey) && !empty($identity)) {
$settings = [
'secret_key' => $secretKey,
'identity' => $identity,
];
}
return $settings;
}
function csrf_is_ready(): bool {
return !empty(csrf_settings());
}
function csrf_token(): string {
static $token = null;
if(empty($token)) {
$settings = csrf_settings();
$token = csrf_token_create(
$settings['identity'],
$settings['secret_key']
);
}
return $token;
}
function csrf_verify(string $token): bool {
$settings = csrf_settings();
return csrf_token_verify(
$token,
$settings['identity'],
$settings['secret_key']
);
}
function csrf_verify_request(?string $token = null): bool {
if(empty($token)) {
$token = $_SERVER['HTTP_X_MISUZU_CSRF'] ?? $_REQUEST['csrf'] ?? '';
}
return csrf_verify($token);
}
function csrf_html(): string {
return sprintf(MSZ_CSRF_HTML, csrf_token());
}
function csrf_http_header(string $name = 'X-Misuzu-CSRF'): void {
header("{$name}: " . csrf_token());
}

View file

@ -189,8 +189,8 @@ function url_variable(string $value, array $variables): string {
return constant(trim($value, '[]'));
}
if(starts_with($value, '{') && ends_with($value, '}') && csrf_is_ready()) {
return csrf_token();
if(starts_with($value, '{') && ends_with($value, '}')) {
return \Misuzu\CSRF::token();
}
return $value;

View file

@ -4,9 +4,10 @@
{% endspaceless %}
{% endmacro %}
{% macro input_csrf() %}{# so we don't have to specify |raw every time #}
{% macro input_csrf() %}
{% from _self import input_hidden %}
{% spaceless %}
{{ csrf_input()|raw }}
{{ input_hidden('_csrf', csrf_token()) }}
{% endspaceless %}
{% endmacro %}