Handle passwords in-transit Base64 encoded to allow arbitrary characters / escape sequences (fixes #2892)

This commit is contained in:
Bernd Bestel 2026-03-31 21:19:25 +02:00
parent 763676c936
commit d4bf5d075a
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
7 changed files with 56 additions and 18 deletions

View File

@ -117,7 +117,9 @@ else
$authMiddlewareClass = GROCY_AUTH_CLASS;
$app->add(new $authMiddlewareClass($container, $app->getResponseFactory()));
// Add default middleware
$app->addBodyParsingMiddleware();
$app->addRoutingMiddleware();
$errorMiddleware = $app->addErrorMiddleware(true, false, false);
$errorMiddleware->setDefaultErrorHandler(

View File

@ -52,7 +52,7 @@
### General
- xxx
- Fixed that it wasn't possible to log in using passwords containing special escape sequences (e.g. `<<`)
### API

View File

@ -22,7 +22,15 @@ class LoginController extends BaseController
public function ProcessLogin(Request $request, Response $response, array $args)
{
$authMiddlewareClass = GROCY_AUTH_CLASS;
if ($authMiddlewareClass::ProcessLogin($request->getParsedBody()))
$postParams = $request->getParsedBody();
if (isset($postParams['password_base64']))
{
$postParams['password'] = base64_decode($postParams['password_base64']);
}
unset($postParams['password_base64']);
if ($authMiddlewareClass::ProcessLogin($postParams))
{
return $response->withRedirect($this->AppContainer->get('UrlManager')->ConstructUrl('/'));
}

View File

@ -43,6 +43,12 @@ class UsersApiController extends BaseApiController
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
}
if (isset($requestBody['password_base64']))
{
$requestBody['password'] = base64_decode($requestBody['password_base64']);
}
unset($requestBody['password_base64']);
$this->getUsersService()->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password'], $requestBody['picture_file_name']);
return $this->EmptyApiResponse($response);
}
@ -81,6 +87,12 @@ class UsersApiController extends BaseApiController
try
{
if (isset($requestBody['password_base64']))
{
$requestBody['password'] = base64_decode($requestBody['password_base64']);
}
unset($requestBody['password_base64']);
$this->getUsersService()->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password'], $requestBody['picture_file_name']);
return $this->EmptyApiResponse($response);
}

View File

@ -1,4 +1,4 @@
setTimeout(function()
setTimeout(function ()
{
$('#username').focus();
}, Grocy.FormFocusDelay);
@ -8,3 +8,11 @@ if (GetUriParam('invalid') === 'true')
$('#login-error').text(__t('Invalid credentials, please try again'));
$('#login-error').removeClass('d-none');
}
$("#login-button").on("click", function (e)
{
e.preventDefault();
$("#password_base64").val(btoa($("#password_input").val()));
$("#login-form").trigger("submit");
});

View File

@ -24,7 +24,7 @@
});
}
$('#save-user-button').on('click', function(e)
$('#save-user-button').on('click', function (e)
{
e.preventDefault();
@ -46,11 +46,16 @@ $('#save-user-button').on('click', function(e)
jsonData.picture_file_name = RandomString() + CleanFileName($("#user-picture")[0].files[0].name);
}
jsonData.password_base64 = btoa(jsonData.password);
delete jsonData.password;
delete jsonData.password_confirm;
delete jsonData.change_password;
if (Grocy.EditMode === 'create')
{
Grocy.Api.Post('users', jsonData,
(result) => SaveUserPicture(result, jsonData),
function(xhr)
function (xhr)
{
Grocy.FrontendHelpers.EndUiBusy("user-form");
console.error(xhr);
@ -64,11 +69,11 @@ $('#save-user-button').on('click', function(e)
jsonData.picture_file_name = null;
Grocy.Api.DeleteFile(Grocy.UserPictureFileName, 'userpictures',
function(result)
function (result)
{
// Nothing to do
},
function(xhr)
function (xhr)
{
Grocy.FrontendHelpers.EndUiBusy("user-form");
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
@ -78,7 +83,7 @@ $('#save-user-button').on('click', function(e)
Grocy.Api.Put('users/' + Grocy.EditObjectId, jsonData,
(result) => SaveUserPicture(result, jsonData),
function(xhr)
function (xhr)
{
Grocy.FrontendHelpers.EndUiBusy("user-form");
console.error(xhr);
@ -87,7 +92,7 @@ $('#save-user-button').on('click', function(e)
}
});
$('#user-form input').keyup(function(event)
$('#user-form input').keyup(function (event)
{
var element = document.getElementById("password_confirm");
if ($("#password").val() !== $("#password_confirm").val())
@ -102,7 +107,7 @@ $('#user-form input').keyup(function(event)
Grocy.FrontendHelpers.ValidateForm('user-form');
});
$('#user-form input').keydown(function(event)
$('#user-form input').keydown(function (event)
{
if (event.keyCode === 13) // Enter
{
@ -119,7 +124,7 @@ $('#user-form input').keydown(function(event)
}
});
$("#user-picture").on("change", function(e)
$("#user-picture").on("change", function (e)
{
$("#user-picture-label").removeClass("d-none");
$("#user-picture-label-none").addClass("d-none");
@ -129,7 +134,7 @@ $("#user-picture").on("change", function(e)
});
Grocy.DeleteUserPictureOnSave = false;
$("#delete-current-user-picture-button").on("click", function(e)
$("#delete-current-user-picture-button").on("click", function (e)
{
Grocy.DeleteUserPictureOnSave = true;
$("#current-user-picture").addClass("d-none");
@ -138,12 +143,12 @@ $("#delete-current-user-picture-button").on("click", function(e)
$("#user-picture-label-none").removeClass("d-none");
});
$("#change_password").click(function()
$("#change_password").click(function ()
{
$("#password").attr("disabled", !this.checked);
$("#password_confirm").attr("disabled", !this.checked);
setTimeout(function()
setTimeout(function ()
{
$("#password").focus();
}, Grocy.FormFocusDelay);
@ -155,7 +160,7 @@ if (GetUriParam("changepw") === "true")
}
else
{
setTimeout(function()
setTimeout(function ()
{
$('#username').focus();
}, Grocy.FormFocusDelay);

View File

@ -23,13 +23,16 @@
name="username">
</div>
<input type="hidden"
id="password_base64"
name="password_base64">
<div class="form-group">
<label for="password">{{ $__t('Password') }}</label>
<label for="password_input">{{ $__t('Password') }}</label>
<input type="password"
class="form-control"
required
id="password"
name="password">
id="password_input">
<div id="login-error"
class="form-text text-danger d-none"></div>
</div>