Prevent delpass bruteforcing

This commit is contained in:
Juribiyan 2020-02-18 00:44:47 +05:00
parent 9822874d3d
commit b16b6a7362
12 changed files with 270 additions and 86 deletions

View File

@ -0,0 +1,4 @@
ALTER TABLE `posts`
ADD COLUMN `attempts` TINYINT(3) UNSIGNED NULL DEFAULT '0' AFTER `by_new_user`;
UPDATE `posts` SET `attempts`=0 WHERE 1;

156
board.php
View File

@ -83,45 +83,47 @@ function notify($room, $data=array()) {
}
class PolymorphicReporter {
function __construct($itemtype, $id, $is_ajax) {
$this->itemtype = $itemtype;
$this->id = $id;
$this->is_ajax = $is_ajax;
}
function __construct($itemtype, $id, $is_ajax) {
$this->itemtype = $itemtype;
$this->id = $id;
$this->is_ajax = $is_ajax;
}
function fail($msg="") {
if ($this->is_ajax) {
$this->success = false;
$this->message = $msg;
}
else
echo $this->itemtype . ' #' . $this->id . ': ' . $msg . '<br />';
}
function fail($msg="", $special_error=false) {
$this->special_error = $special_error;
if ($this->is_ajax) {
$this->success = false;
$this->message = $msg;
}
else
echo $this->itemtype . ' #' . $this->id . ': ' . $msg . '<br />';
}
function succ($msg="") {
if ($this->is_ajax) {
$this->success = true;
$this->message = $msg;
}
else
echo $this->itemtype . ' #' . $this->id . ': ' . $msg . '<br />';
}
function succ($msg="") {
if ($this->is_ajax) {
$this->success = true;
$this->message = $msg;
}
else
echo $this->itemtype . ' #' . $this->id . ': ' . $msg . '<br />';
}
function report() {
return array(
'id' => $this->id,
'itemtype' => $this->itemtype,
'action' => $this->action,
'success' => $this->success,
'message' => $this->message
);
}
function report() {
return array(
'id' => $this->id,
'itemtype' => $this->itemtype,
'action' => $this->action,
'success' => $this->success,
'message' => $this->message,
'special_error' => $this->special_error
);
}
}
function error_redirect($url, $message) {
if ($_POST['AJAX']) {
exit(json_encode(array(
'error' => $message
'error' => $message
)));
}
else {
@ -159,8 +161,8 @@ $posting_class = new Posting();
$ban_result = $bans_class->BanCheck($posting_class->user_id, $board_class->board['name']);
if ($ban_result && is_array($ban_result) && $_POST['AJAX']) {
exit(json_encode(array(
'error' => _gettext('YOU ARE BANNED'),
'error_type' => 'ban'
'error' => _gettext('YOU ARE BANNED'),
'error_type' => 'ban'
)));
}
@ -357,9 +359,9 @@ if (isset($_POST['makepost'])) { // A more evident way to identify post action,
// First array is the converted form of the japanese characters meaning sage, second meaning age
$ords_email = unistr_to_ords($post_email);
if (strtolower($_POST['em']) != 'sage' && $ords_email != array(19979, 12370) && strtolower($_POST['em']) != 'age' && $ords_email != array(19978, 12370) && $_POST['em'] != 'return' && $_POST['em'] != 'noko') {
$post['email_save'] = true;
$post['email_save'] = true;
} else {
$post['email_save'] = false;
$post['email_save'] = false;
}
$post['subject'] = mb_substr($post_subject, 0, KU_MAXSUBJLENGTH);
$post['message'] = $post_message;
@ -368,13 +370,13 @@ if (isset($_POST['makepost'])) { // A more evident way to identify post action,
// I never knew this weird shit exists in Kusaba
if ($thread_replyto != '0') {
if ($post['message'] == '' && KU_NOMESSAGEREPLY != '') {
$post['message'] = KU_NOMESSAGEREPLY;
}
if ($post['message'] == '' && KU_NOMESSAGEREPLY != '') {
$post['message'] = KU_NOMESSAGEREPLY;
}
} else {
if ($post['message'] == '' && KU_NOMESSAGETHREAD != '') {
$post['message'] = KU_NOMESSAGETHREAD;
}
if ($post['message'] == '' && KU_NOMESSAGETHREAD != '') {
$post['message'] = KU_NOMESSAGETHREAD;
}
}
// Emoji registration
@ -407,7 +409,7 @@ if (isset($_POST['makepost'])) { // A more evident way to identify post action,
}
// Reparse post
if ($any_new)
$post['message'] = $parse_class->Smileys($post['message']);
$post['message'] = $parse_class->Smileys($post['message']);
}
// ← Emoji registration
@ -434,14 +436,14 @@ if (isset($_POST['makepost'])) { // A more evident way to identify post action,
}
if ($user_authority > 0 && $user_authority != 3) {
$modpost_message = 'Modposted #<a href="' . KU_BOARDSFOLDER . $board_class->board['name'] . '/res/';
if ($post_isreply) {
$modpost_message .= $thread_replyto;
} else {
$modpost_message .= $post_id;
}
$modpost_message .= '.html#' . $post_id . '">' . $post_id . '</a> in /'.$_POST['board'].'/ with flags: ' . $flags . '.';
management_addlogentry($modpost_message, 1, md5_decrypt($_POST['modpassword'], KU_RANDOMSEED));
$modpost_message = 'Modposted #<a href="' . KU_BOARDSFOLDER . $board_class->board['name'] . '/res/';
if ($post_isreply) {
$modpost_message .= $thread_replyto;
} else {
$modpost_message .= $post_id;
}
$modpost_message .= '.html#' . $post_id . '">' . $post_id . '</a> in /'.$_POST['board'].'/ with flags: ' . $flags . '.';
management_addlogentry($modpost_message, 1, md5_decrypt($_POST['modpassword'], KU_RANDOMSEED));
}
// Give persistent cookie
@ -449,11 +451,11 @@ if (isset($_POST['makepost'])) { // A more evident way to identify post action,
setcookie('I0_persistent_id', $posting_class->user_id, time() + 31556926, '/'/*, KU_DOMAIN*/);
if ($post['name_save'] && isset($_POST['name'])) {
setcookie('name', $_POST['name'], time() + 31556926, '/', KU_DOMAIN);
setcookie('name', $_POST['name'], time() + 31556926, '/', KU_DOMAIN);
}
if ($post['email_save']) {
setcookie('email', $post['email'], time() + 31556926, '/', KU_DOMAIN);
setcookie('email', $post['email'], time() + 31556926, '/', KU_DOMAIN);
}
setcookie('postpassword', $_POST['postpassword'], time() + 31556926, '/');
@ -525,6 +527,7 @@ elseif (
$pages_to_regenerate = array(); // single pages to regenerate
$page_from = false;
$page_to = false;
$captcha_ok = false;
// Check rights
$pass = (isset($_POST['postpassword']) && $_POST['postpassword']!="") ? $_POST['postpassword'] : null;
@ -575,10 +578,32 @@ elseif (
$isop = false;
$pwd_ref_post = $post_class;
}
if ($pass) {
// Determine if post is locked due to many access attempts
$locked = !$ismod && $post_class->CheckAccessLocked();
$unlocked = !$locked;
if ($locked) {
if ($captcha_ok === false) {
if ($_POST['captcha']) {
$captcha_ok = $posting_class->CheckCaptcha(true);
if (!$captcha_ok) {
$post_action->fail(_gettext('Incorrect captcha entered.'), 'captchalocked');
}
else {
$unlocked = true;
}
}
else {
$post_action->fail(_gettext('Insert captcha.'), 'captchalocked');
}
}
else {
$unlocked = $captcha_ok;
}
}
if ($pass && $unlocked) {
$passtype = $pwd_ref_post->post['password'] [0];
if ($passtype == '+') { // modern hash with salt: +md5(password+postid+boardid+randomseed)
$pass_for_this_post = '+'.md5($pass . $pwd_ref_post->post['id'] . $board_class->board['id'] . KU_RANDOMSEED);
$pass_for_this_post = '+'.md5($pass . $pwd_ref_post->post['id'] . $board_class->board['id'] . KU_RANDOMSEED);
}
elseif ($passtype == '-') { // modern hash w/o salt: -md5(password+randomseed)
if (!$passmd5_new)
@ -591,7 +616,7 @@ elseif (
$pass_for_this_post = $passmd5_old;
}
}
$granted = ($ismod || ($pass && $pass_for_this_post == $pwd_ref_post->post['password']));
$granted = $unlocked && ($ismod || ($pass && $pass_for_this_post == $pwd_ref_post->post['password']));
if ($granted) {
$thread_id = $post_class->post['parentid'] != '0' ? $post_class->post['parentid'] : $post_class->post['id'];
$room_id = $board_class->board['name'].':'.$thread_id;
@ -611,6 +636,9 @@ elseif (
'by_mod' => $ismod,
'by_op' => $isop
);
if ($locked) {
$post_class->Unlock();
}
if (! in_array($thread_id, $threads_to_regenerate)) {
$threads_to_regenerate []= $thread_id;
}
@ -631,7 +659,7 @@ elseif (
if (isset($_POST['deletepost'])) {
$post_action->action = 'delete';
$isownpost = !$ismod && !$isop;
$delres = $post_class->Delete(false, $isownpost && I0_ERASE_DELETED);
$delres = $post_class->Delete(false, $isownpost && I0_ERASE_DELETED, $ismod);
if ($delres) {
if ($delres !== 'already_deleted') { // Skip the unneeded rebuild if the post is already deleted
if (! isset($notifications_del[$room_id]))
@ -721,7 +749,7 @@ elseif (
}
}
}
else {
elseif ($post_action->special_error !== 'captchalocked') {
$post_action->fail(_gettext('Incorrect password.'));
}
}
@ -783,8 +811,8 @@ elseif (
}
if ($_POST['AJAX'])
exit(json_encode(array(
'action' => 'multi_post_action',
'data' => $items_affected
'action' => 'multi_post_action',
'data' => $items_affected
)));
else
do_redirect(KU_BOARDSPATH . '/' . $board_class->board['name'] . '/');
@ -818,10 +846,10 @@ if( $_POST['redirecttothread'] == 1 || $_POST['em'] == 'return' || $_POST['em']
if ($_POST['AJAX']) {
exit(json_encode(array(
'error' => false,
'action' => 'post',
'thread_replyto' => $thread_replyto,
'post_id' => $post_id,
'board' => $board_class->board['name']
'error' => false,
'action' => 'post',
'thread_replyto' => $thread_replyto,
'post_id' => $post_id,
'board' => $board_class->board['name']
)));
}

View File

@ -99,6 +99,7 @@ if (!$cache_loaded) {
$cf['I0_DELPASS_SALTING'] = true; // Whether or not the delpass should be hashed to prevent poster identification
$cf['I0_ERASE_DELETED'] = false; // Whether or not the contents of posts deleted by user should be erased
$cf['I0_MAX_ACCESS_ATTEMPTS'] = 3; // How many attempts at deleting a post are allowed before it gets locked with catpcha
$cf['I0_DETECT_SOSACH'] = false; // Detect pictures from particular website

View File

@ -2810,6 +2810,22 @@ figure .post-menu {
.post-menu li .icon {
margin-right: 5px;
}
.menu-captcha {
display: -webkit-box;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
-webkit-box-align: center;
align-items: center;
}
.menu-captcha .captchawrap, .menu-captcha input {
margin-bottom: 2px;
}
.menu-captcha input {
margin-bottom: 2px;
width: 100%;
}
.select-multiple .multidel {
display: initial;
}

View File

@ -3,6 +3,11 @@
<tr><td>
<input placeholder="{t}Password{/t}" type="password" name="postpassword" size="8" class="make-me-readonly"/>
</td></tr>
<noscript><tr><td><details>
<summary>{t}Captcha{/t}</summary>
<iframe class="captchawrap" src="{%KU_BOARDSFOLDER}nojscaptcha.php" frameborder="0" width="150" height="32" style="vertical-align: middle;"></iframe><br>
<input type="text" name="captcha" placeholder="{t}Captcha{/t}" style="margin-top:4px" accesskey="c" style="vertical-align: middle" autocomplete="off">
</details></td></tr></noscript>
<tr><td>
<input name="deletepost" value="{t}Delete post{/t}" type="submit" class="styled-button bad-button" />{if $board.opmod}<label for="opmod">(<input type="checkbox" id="opmod" name="opdelete" value="1">{t}as OP{/t})</label>
{/if}
@ -16,7 +21,6 @@
<input name="cancel_timer" value="{t}Cancel timer{/t}" type="submit" class="styled-button" />
</td></tr>
</tbody></table>
</form>
<script type="text/javascript"><!--

View File

@ -70,7 +70,7 @@
<td class="postblock"></td>
<td><nobr class="captcharow">
<input type="text" name="captcha" placeholder="{t}Captcha{/t}" size="28" accesskey="c" style="vertical-align: middle" autocomplete="off">
<script>document.write('<div class="captchawrap cw-initial" title="{t}Refresh captcha{/t}"><div class="captcha-show msg">{t}Show captcha{/t}</div><img class="captchaimage" valign="middle" border="0" alt="Captcha image"><div class="rotting-indicator"></div><div class="rotten-msg msg">{t}Captcha has expired{t}.</div></div>')</script>
<script>document.write('<div class="captchawrap cw-initial" title="{t}Refresh captcha{/t}"><div class="captcha-show msg">{t}Show captcha{/t}</div><img class="captchaimage" valign="middle" border="0" alt="{t}Captcha image{/t}"><div class="rotting-indicator"></div><div class="rotten-msg msg">{t}Captcha has expired{t}.</div></div>')</script>
<noscript><iframe class="captchawrap" src="{%KU_BOARDSFOLDER}nojscaptcha.php" frameborder="0" width="150" height="32" style="vertical-align: middle;"></iframe></noscript>
</nobr></td>
</tr>
@ -159,7 +159,7 @@
{t}Password{/t}
</td>
<td>
<input class="make-me-readonly" type="password" placeholder="{t}Password{/t}" name="postpassword" size="8" accesskey="p" /><div><span>{t}(for post and file deletion){/t}</span></div>
<input class="make-me-readonly" type="password" placeholder="{t}Password{/t}" name="postpassword" size="28" accesskey="p" /><div><span>{t}(for post and file deletion){/t}</span></div>
</td>
</tr>
<tr class="ttl-row">

View File

@ -973,7 +973,37 @@ class Post extends Board {
`country` = '',
`password` = ''";
function Delete($allow_archive = false, $erase = false) {
function CheckAccessLocked() {
global $tc_db;
$attempts = $tc_db->GetOne("SELECT `attempts`
FROM `".KU_DBPREFIX."posts`
WHERE
`id` = ".$this->post['id']." AND
`boardid` = ".$this->board['id']);
if ((int)$attempts >= I0_MAX_ACCESS_ATTEMPTS) {
return true;
}
else {
$tc_db->Execute("UPDATE `".KU_DBPREFIX."posts`
SET `attempts` = `attempts`+1
WHERE
`id` = ".$this->post['id']." AND
`boardid` = ".$this->board['id']);
return false;
}
}
function Unlock() {
global $tc_db;
$tc_db->Execute("UPDATE `".KU_DBPREFIX."posts`
SET `attempts` = 0
WHERE
`id` = ".$this->post['id']." AND
`boardid` = ".$this->board['id']);
}
function Delete($allow_archive = false, $erase = false) {
global $tc_db;
if ($this->post['IS_DELETED'])
return 'already_deleted';

View File

@ -208,22 +208,26 @@ class Posting {
}
}
function CheckCaptcha() {
function CheckCaptcha($for_access=false) {
global $board_class;
mb_internal_encoding("UTF-8");
/* If the board has captcha's enabled... */
if ($board_class->board['enablecaptcha'] == 1) {
if ($for_access || $board_class->board['enablecaptcha'] == 1) {
$code = $_SESSION['security_code'];
unset($_SESSION['security_code']);
$submit_time = time();
if($submit_time - $_SESSION['captchatime'] > KU_CAPTCHALIFE) {
if ($for_access) return false;
exitWithErrorPage(_gettext('Captcha has expired.'));
}
/* Check if they entered the correct code. If not... */
if ($_SESSION['security_code'] != mb_strtoupper($_POST['captcha']) || empty($_SESSION['security_code'])) {
if ($code != mb_strtoupper($_POST['captcha']) || empty($code)) {
if ($for_access) return false;
/* Kill the script, stopping the posting process */
exitWithErrorPage(_gettext('Incorrect captcha entered.'));
}
}
unset($_SESSION['security_code']);
if ($for_access) return true;
}
function CheckRecaptcha() { //just backup

View File

@ -2148,3 +2148,6 @@ msgstr "Отменить таймер"
msgid "as OP"
msgstr "как ОП"
msgid "Insert captcha."
msgstr "Введите капчу."

View File

@ -125,7 +125,12 @@ var _messages = {
cancelTimer: 'Cancel timer',
saved:'saved from deletion',
savedMulti: 'saved from deletion',
password: 'Password'
password: 'Password',
captcha: 'Captcha',
captchaImage: 'Captcha image',
refreshCaptcha: 'Refresh captcha',
showCaptcha: 'Show captcha',
captchaExpired: 'Captcha has expired.'
},
ru: {
noLocalStorage: "localStorage не поддерживается браузером",
@ -245,7 +250,12 @@ var _messages = {
cancelTimer: 'Отменить таймер',
saved:'спасен от удаления',
savedMulti: 'спасены от удаления',
password: 'Пароль'
password: 'Пароль',
captcha: 'Captcha',
captchaImage: 'Captcha image',
refreshCaptcha: 'Обновить капчу',
showCaptcha: 'Показать капчу',
captchaExpired: 'Капча протухла.'
}
}
var _l = (typeof locale !== 'undefined' && _messages.hasOwnProperty(locale)) ? _messages[locale] : _messages.ru;
@ -526,10 +536,12 @@ function highlight(id, offTimeout=5000) {
return true
}
const password_length = 20;
function get_password(name) {
let pass = getCookie(name);
if(pass) return pass;
pass = randomString(8)
pass = randomString(password_length)
Cookie(name, pass, 365);
return(pass);
}
@ -1064,6 +1076,7 @@ function clonePostForm(anchorID, preview) {
newForm.dataset.anchorID = anchorID
newForm.classList.remove('main-reply-form')
newForm.querySelector('.simplified-send-row .primary span').innerText = _l.reply
newForm.querySelector('input[name="postpassword"]').value = get_password('postpassword')
newForm.querySelector('input.primary').value = _l.reply
let blotter = newForm.querySelector('.blotter-row')
if (preview)
@ -1291,9 +1304,9 @@ function popupMessage(content, delay=1000) {
}
var Captcha = {
init: function() {
init: function(forceEnable=false) {
let captchaImage = document.querySelector('.captchaimage')
this.enabled = !!captchaImage
this.enabled = forceEnable || !!captchaImage
if (!this.enabled) return;
injector.inject('captcha-rotting',
`.cw-running .rotting-indicator {
@ -1304,16 +1317,25 @@ var Captcha = {
.cw-running .rotten-msg {
-webkit-animation-delay: ${captchaTimeout}s;
animation-delay: ${captchaTimeout}s;}`)
captchaImage.onload = this.onImageLoad.bind(this)
;['animationend', 'webkitAnimationEnd', 'msAnimationEnd'].forEach(evType => {
captchaImage.addEventListener(evType, this.onAnimationEnd.bind(this))
})
if (captchaImage) {
this.addImgLoadListener(captchaImage)
}
},
initForm: function(form) {
form.querySelector('.captchawrap').onclick = this.onClick.bind(this)
;['click', 'focus'].forEach(evt => {
form.querySelector('input[name=captcha]').addEventListener(evt, this.onFieldClick.bind(this))
})
let captchaImage = form.querySelector('.captchaimage')
if (captchaImage) {
this.addImgLoadListener(captchaImage)
}
},
addImgLoadListener: function(captchaImage) {
captchaImage.onload = this.onImageLoad.bind(this)
;['animationend', 'webkitAnimationEnd', 'msAnimationEnd'].forEach(evType => {
captchaImage.addEventListener(evType, this.onAnimationEnd.bind(this))
})
},
_state: 'init',
get state() {
@ -1961,6 +1983,32 @@ function makeIcon(i, classes="", bare=false) {
${bare ? '' : '</svg>'}`
}
function addCaptchaToMenu($menu) {
if ($menu.find('.captchawrap').length) {
Captcha.state = 'init';
return;
}
if (!Captcha.enabled) {
Captcha.init('only-access')
}
$menu.prepend(`<li class="menu-captcha">
<div class="captchawrap cw-initial captchaimage-invisible" title="${_l.refreshCaptcha}">
<div class="captcha-show msg">${_l.showCaptcha}</div>
<img class="captchaimage" valign="middle" border="0" alt="${_l.captchaImage}">
<div class="rotting-indicator"></div>
<div class="rotten-msg msg">${_l.captchaExpired}</div>
</div>
<input type="text" name="captcha" placeholder="${_l.captcha}" autocomplete="off">
</li>`)
Captcha.initForm($menu[0])
$menu.find('input[name=captcha]').keydown(ev => {
if (ev.key == 'Enter') {
ev.preventDefault()
ev.stopPropagation()
}
})
}
function expandimg(postnum, imgurl, thumburl, imgw, imgh, thumbw, thumbh) {
let element = document.getElementById("thumb" + postnum);
if (element == null) return false;
@ -2632,6 +2680,16 @@ function readyset() {
, $pass = $menu.find('.menu-password')
$this.addClass('spin-around')
let fd = new FormData()
if ($menu.find('.menu-captcha').length) {
let $c = $menu.find('input[name=captcha]')
if (!$c.val()) {
$c.focus()
pups.warn(_l.enterCaptcha)
$this.removeClass('spin-around')
return;
}
fd.append('captcha', $c.val())
}
if (isFile)
fd.append('delete-file[]', menu.__menuProps.fileid)
else
@ -2655,7 +2713,18 @@ function readyset() {
$menu.prepend(`<li class="menu-password">${makeIcon('password')}
<input value="${get_password("postpassword")}" class="make-me-readonly" type="password" placeholder="${_l.password}" readonly onfocus="this.readOnly=false"></li>`)
}
$menu.find('.menu-password input').select()
if (result.special_error && result.special_error == 'captchalocked') {
addCaptchaToMenu($menu)
}
else {
$menu.find('.menu-password input').select()
.keydown(ev => {
if (ev.key == 'Enter') {
ev.preventDefault()
ev.stopPropagation()
}
})
}
}
else if (isFile)
$('.file-menu').hide()
@ -2691,6 +2760,16 @@ function readyset() {
, $postnode = $this.parents('.postnode')
$this.addClass('spin-around')
let fd = new FormData()
if ($menu.find('.menu-captcha').length) {
let $c = $menu.find('input[name=captcha]')
if (!$c.val()) {
$c.focus()
pups.warn(_l.enterCaptcha)
$this.removeClass('spin-around')
return;
}
fd.append('captcha', $c.val())
}
fd.append('post[]', $postnode.data('id'))
fd.append('board', $postnode.data('board'))
fd.append('modsave', $this.hasClass('menu-delete-mod'))
@ -2701,11 +2780,26 @@ function readyset() {
Ajax.cancelTimer(fd, errors => {
$this.removeClass('spin-around')
if (errors.length) {
let result = errors[0]
if (!$pass.length) {
$menu.prepend(`<li class="menu-password">${makeIcon('password')}
<input value="${get_password("postpassword")}" class="make-me-readonly" type="password" placeholder="${_l.password}" readonly onfocus="this.readOnly=false"></li>`)
}
$menu.find('.menu-password input').select()
if (result.special_error && result.special_error == 'captchalocked') {
addCaptchaToMenu($menu)
}
else {
$menu.find('.menu-password input').select()
.keydown(ev => {
if (ev.key == 'Enter') {
ev.preventDefault()
ev.stopPropagation()
}
})
}
}
else {
$menu.find('.menu-password, .menu-captcha').remove()
}
})
})
@ -3953,7 +4047,7 @@ function initForm(form) {
form.querySelector('textarea').id = areaID
form.querySelector('.uib-tx').dataset.target = areaID
// captcha
if (Captcha.enabled)
if (Captcha.enabled && Captcha.enabled!=='only-access')
Captcha.initForm(form)
// readonly stuff
form.querySelectorAll('.make-me-readonly').forEach(ro => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long