Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

login: Fix CSRF fail, add shake effect on authentication fail #3955

Merged
merged 2 commits into from Sep 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 9 additions & 4 deletions include/class.ostsession.php
Expand Up @@ -178,10 +178,15 @@ class DbSessionBackend

function read($id) {
try {
$this->data = SessionData::objects()->filter([
'session_id' => $id,
'session_expire__gt' => SqlFunction::NOW(),
])->one();
$this->data = SessionData::objects()
->filter(['session_id' => $id])
->annotate(['age' => SqlFunction::NOW()->minus(new SqlField('session_expire'))])
->one();
if ($this->data->age > 0) {
// session_expire is in the past. Pretend it is expired and
// reset the data. This will assist with CSRF issues
$this->data->session_data='';
}
$this->id = $id;
}
catch (DoesNotExist $e) {
Expand Down
87 changes: 81 additions & 6 deletions include/staff/login.tpl.php
Expand Up @@ -11,20 +11,21 @@
<span class="valign-helper"></span>
<img src="logo.php?login" alt="osTicket :: <?php echo __('Staff Control Panel');?>" />
</a></h1>
<h3><?php echo Format::htmlchars($msg); ?></h3>
<h3 id="login-message"><?php echo Format::htmlchars($msg); ?></h3>
<div class="banner"><small><?php echo ($content) ? Format::display($content->getLocalBody()) : ''; ?></small></div>
<form action="login.php" method="post" id="login">
<form action="login.php" method="post" id="login" onsubmit="attemptLoginAjax(event)">
<?php csrf_token(); ?>
<input type="hidden" name="do" value="scplogin">
<fieldset>
<input type="text" name="userid" id="name" value="<?php
echo $info['userid']; ?>" placeholder="<?php echo __('Email or Username'); ?>"
autofocus autocorrect="off" autocapitalize="off">
<input type="password" name="passwd" id="pass" placeholder="<?php echo __('Password'); ?>" autocorrect="off" autocapitalize="off">
<?php if ($show_reset && $cfg->allowPasswordReset()) { ?>
<h3 style="display:inline"><a href="pwreset.php"><?php echo __('Forgot My Password'); ?></a></h3>
<?php } ?>
<button class="submit button pull-right" type="submit" name="submit"><i class="icon-signin"></i>
<h3 style="display:inline"><a id="reset-link" class="<?php
if (!$show_reset || !$cfg->allowPasswordReset()) echo 'hidden';
?>" href="pwreset.php"><?php echo __('Forgot My Password'); ?></a></h3>
<button class="submit button pull-right" type="submit"
name="submit"><i class="icon-signin"></i>
<?php echo __('Log In'); ?>
</button>
</fieldset>
Expand Down Expand Up @@ -61,11 +62,85 @@
document.getElementById('loginBox').style.backgroundColor = 'white';
}
});

function attemptLoginAjax(e) {
var objectifyForm = function(formArray) { //serialize data function
var returnArray = {};
for (var i = 0; i < formArray.length; i++) {
returnArray[formArray[i]['name']] = formArray[i]['value'];
}
return returnArray;
};
if ($.fn.effect) {
// For some reason, JQuery-UI shake does not considere an element's
// padding when shaking. Looks like it might be fixed in 1.12.
// Thanks, https://stackoverflow.com/a/22302374
var oldEffect = $.fn.effect;
$.fn.effect = function (effectName) {
if (effectName === "shake") {
var old = $.effects.createWrapper;
$.effects.createWrapper = function (element) {
var result;
var oldCSS = $.fn.css;

$.fn.css = function (size) {
var _element = this;
var hasOwn = Object.prototype.hasOwnProperty;
return _element === element && hasOwn.call(size, "width") && hasOwn.call(size, "height") && _element || oldCSS.apply(this, arguments);
};

result = old.apply(this, arguments);

$.fn.css = oldCSS;
return result;
};
}
return oldEffect.apply(this, arguments);
};
}
var form = $(e.target),
data = objectifyForm(form.serializeArray())
data.ajax = 1;
$.ajax({
url: form.attr('action'),
method: 'POST',
data: data,
cache: false,
success: function(json) {
if (!typeof(json) === 'object' || !json.status)
return;
switch (json.status) {
case 401:
if (json && json.redirect)
document.location.href = json.redirect;
if (json && json.message)
$('#login-message').text(json.message)
if (json && json.show_reset)
$('#reset-link').show()
if ($.fn.effect) {
$('#loginBox').effect('shake')
}
// Clear the password field
$('#pass').val('').focus();
break
case 302:
if (json && json.redirect)
document.location.href = json.redirect;
break
}
},
});
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
return false;
}
</script>
<!--[if IE]>
<style>
#loginBox:after { background-color: white !important; }
</style>
<![endif]-->
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions scp/css/login.css
Expand Up @@ -353,3 +353,6 @@ input[type=password] {
padding: 5px;
font-size: 0.75em;
}
.hidden {
display: none;
}
50 changes: 39 additions & 11 deletions scp/login.php
Expand Up @@ -30,29 +30,57 @@
$msg = $msg ?: ($content ? $content->getLocalName() : __('Authentication Required'));
$dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php';
$show_reset = false;
if($_POST) {
if ($_POST) {
$json = isset($_POST['ajax']) && $_POST['ajax'];
$respond = function($code, $message) use ($json, $ost) {
if ($json) {
$payload = is_array($message) ? $message
: array('message' => $message);
$payload['status'] = (int) $code;
Http::response(200, JSONDataEncoder::encode($payload),
'application/json');
}
else {
// Extract the `message` portion only
if (is_array($message))
$message = $message['message'];
Http::response($code, $message);
}
};
$redirect = function($url) use ($json) {
if ($json)
Http::response(200, JsonDataEncoder::encode(array(
'status' => 302, 'redirect' => $url)), 'application/json');
else
Http::redirect($url);
};

// Check the CSRF token, and ensure that future requests will have to
// use a different CSRF token. This will help ward off both parallel and
// serial brute force attacks, because new tokens will have to be
// requested for each attempt.
if (!$ost->checkCSRFToken())
Http::response(400, __('Valid CSRF Token Required'));

// Rotate the CSRF token (original cannot be reused)
$ost->getCSRF()->rotate();
if (!$ost->checkCSRFToken()) {
$_SESSION['_staff']['auth']['msg'] = __('Valid CSRF Token Required');
$redirect($_SERVER['REQUEST_URI']);
}

// Lookup support backends for this staff
$username = trim($_POST['userid']);
if ($user = StaffAuthenticationBackend::process($username,
$_POST['passwd'], $errors)) {
session_write_close();
Http::redirect($dest);
require_once('index.php'); //Just incase header is messed up.
exit;
$redirect($dest);
}

$msg = $errors['err']?$errors['err']:__('Invalid login');
$msg = $errors['err'] ?: __('Invalid login');
$show_reset = true;

if ($json) {
$respond(401, ['message' => $msg, 'show_reset' => $show_reset]);
}
else {
// Rotate the CSRF token (original cannot be reused)
$ost->getCSRF()->rotate();
}
}
elseif ($_GET['do']) {
switch ($_GET['do']) {
Expand Down