Create an insecure bank application
This commit is contained in:
41
webroot/lib/Controller/BookingOverviewController.php
Normal file
41
webroot/lib/Controller/BookingOverviewController.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Session;
|
||||
use View\BookingOverviewPage;
|
||||
use View\Sendable;
|
||||
|
||||
class BookingOverviewController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/bookings.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$userId = $this->context->session->user->id;
|
||||
$page = new BookingOverviewPage($this->context);
|
||||
|
||||
$sql = Sql::connection();
|
||||
$sql->query('START TRANSACTION');
|
||||
$stmt = $sql->prepare(
|
||||
'SELECT time, type, amount, comment, name as relatedName FROM booking
|
||||
LEFT JOIN user ON booking.related = user.id
|
||||
WHERE affected = ?
|
||||
ORDER BY time, booking.id'
|
||||
);
|
||||
$stmt->execute([$userId]);
|
||||
$page->bookings = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$stmt = $sql->prepare('SELECT balance FROM user WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$page->finalBalance = $stmt->fetch(\PDO::FETCH_ASSOC)['balance'];
|
||||
|
||||
$sql->query('COMMIT');
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
78
webroot/lib/Controller/CashTransactionController.php
Normal file
78
webroot/lib/Controller/CashTransactionController.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\User;
|
||||
use View\CashTransactionPage;
|
||||
use View\MoneyFormatter;
|
||||
use View\Sendable;
|
||||
|
||||
class CashTransactionController extends RestrictedPageController
|
||||
{
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$cashTransactionPage = new CashTransactionPage($this->context);
|
||||
if ($cashTransactionPage->formWasSent) {
|
||||
$cashTransactionPage->fieldCustomer = trim($cashTransactionPage->fieldCustomer);
|
||||
$error = false;
|
||||
|
||||
// find customer
|
||||
$user = User::byName($cashTransactionPage->fieldCustomer);
|
||||
if (empty($user)) {
|
||||
$cashTransactionPage->errorCustomerNotFound = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check amount
|
||||
$amount = MoneyFormatter::parseAmount($cashTransactionPage->fieldAmount);
|
||||
if ($amount === null) {
|
||||
$cashTransactionPage->errorAmountInvalid = true;
|
||||
$error = true;
|
||||
} elseif ($amount === 0) {
|
||||
$cashTransactionPage->errorAmountZero = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check text
|
||||
$text = $cashTransactionPage->fieldText;
|
||||
if (iconv_strlen($text) > 100) {
|
||||
$cashTransactionPage->errorTextTooLong = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// create the actual booking
|
||||
if (!$error) {
|
||||
switch ($this->context->currentPage) {
|
||||
case '/deposit.php':
|
||||
Transaction::run($this->context, function ($transaction) use ($user, $amount, $text) {
|
||||
$transaction->createBooking($user, 1, $amount, null, $text);
|
||||
});
|
||||
break;
|
||||
case '/withdraw.php':
|
||||
$success = Transaction::run($this->context, function ($transaction) use ($user, $amount, $text) {
|
||||
return $transaction->createBooking($user, 2, -$amount, null, $text);
|
||||
});
|
||||
if (!$success) {
|
||||
$cashTransactionPage->errorInsufficientFunds = true;
|
||||
$error = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new \Exception('unknown page url');
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$cashTransactionPage->fieldCustomer = '';
|
||||
$cashTransactionPage->fieldAmount = '';
|
||||
$cashTransactionPage->fieldText = '';
|
||||
|
||||
$cashTransactionPage->success = true;
|
||||
$cashTransactionPage->successCustomer = $user->name;
|
||||
$cashTransactionPage->successAmount = $amount;
|
||||
}
|
||||
}
|
||||
return $cashTransactionPage;
|
||||
}
|
||||
}
|
43
webroot/lib/Controller/LoginController.php
Normal file
43
webroot/lib/Controller/LoginController.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Session;
|
||||
use Model\User;
|
||||
use View\LoginRedirection;
|
||||
use View\LoginPage;
|
||||
use View\Sendable;
|
||||
|
||||
class LoginController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/login.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$loginPage = new LoginPage($this->context);
|
||||
if ($loginPage->formWasSent) {
|
||||
$loginPage->fieldUsername = trim($loginPage->fieldUsername);
|
||||
|
||||
// find user
|
||||
$user = User::byName($loginPage->fieldUsername);
|
||||
|
||||
// check password
|
||||
// (use a dummy hash if no user was found, to make timing attacks harder)
|
||||
$pwHash = $user?->pwHash ?? '$argon2id$v=19$m=65536,t=4,p=1$WmxPVmd5aGdkandaNWZTcA$6hcqXkBJIGgWkcGLdqZeHhkV83JKtn5Ke7jXRS31X2s';
|
||||
|
||||
$pwValid = password_verify($loginPage->fieldPassword, $pwHash);
|
||||
|
||||
if ($pwValid && !empty($user)) {
|
||||
$this->context->session = Session::create($user);
|
||||
return new LoginRedirection($this->context);
|
||||
} else {
|
||||
$loginPage->errorLoginDataInvalid = true;
|
||||
}
|
||||
}
|
||||
return $loginPage;
|
||||
}
|
||||
}
|
21
webroot/lib/Controller/LogoutController.php
Normal file
21
webroot/lib/Controller/LogoutController.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use View\LogoutRedirection;
|
||||
use View\Sendable;
|
||||
|
||||
class LogoutController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/logout.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$this->context->session->destroy();
|
||||
return new LogoutRedirection();
|
||||
}
|
||||
}
|
61
webroot/lib/Controller/RegisterController.php
Normal file
61
webroot/lib/Controller/RegisterController.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Session;
|
||||
use Model\User;
|
||||
use View\LoginRedirection;
|
||||
use View\RegisterPage;
|
||||
use View\Sendable;
|
||||
|
||||
class RegisterController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/register.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$registerPage = new RegisterPage($this->context);
|
||||
if ($registerPage->formWasSent) {
|
||||
$registerPage->fieldUsername = trim($registerPage->fieldUsername);
|
||||
$error = false;
|
||||
|
||||
// check username
|
||||
if (empty($registerPage->fieldUsername)) {
|
||||
$registerPage->errorUsernameEmpty = true;
|
||||
$error = true;
|
||||
} elseif (iconv_strlen($registerPage->fieldUsername) > 20) {
|
||||
$registerPage->errorUsernameTooLong = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check password
|
||||
if ($registerPage->fieldPassword != $registerPage->fieldRepeatPassword) {
|
||||
$registerPage->errorPasswordsMismatch = true;
|
||||
$error = true;
|
||||
} else if (empty($registerPage->fieldPassword)) {
|
||||
$registerPage->errorPasswordEmpty = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// create account
|
||||
if (!$error) {
|
||||
$pwHash = password_hash($registerPage->fieldPassword, PASSWORD_ARGON2ID);
|
||||
$user = User::create($registerPage->fieldUsername, $pwHash);
|
||||
if (!empty($user)) {
|
||||
$this->context->session = Session::create($user);
|
||||
return new LoginRedirection($this->context);
|
||||
} else {
|
||||
$registerPage->errorUsernameInUse = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $registerPage;
|
||||
} else {
|
||||
return $registerPage;
|
||||
}
|
||||
}
|
||||
}
|
38
webroot/lib/Controller/RestrictedPageController.php
Normal file
38
webroot/lib/Controller/RestrictedPageController.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Context;
|
||||
use View\AccessDeniedPage;
|
||||
use View\Sendable;
|
||||
|
||||
abstract class RestrictedPageController
|
||||
{
|
||||
protected ?Context $context = null;
|
||||
|
||||
public function __construct(string $url)
|
||||
{
|
||||
$this->context = Context::init($url);
|
||||
}
|
||||
|
||||
protected function isCurrentPageAllowed(): bool
|
||||
{
|
||||
foreach ($this->context->navigation as $navigationEntry) {
|
||||
if ($navigationEntry->url === $this->context->currentPage) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public final function run(): Sendable
|
||||
{
|
||||
if (!$this->isCurrentPageAllowed()) {
|
||||
return new AccessDeniedPage($this->context);
|
||||
}
|
||||
return $this->runLogic();
|
||||
}
|
||||
|
||||
abstract protected function runLogic(): Sendable;
|
||||
}
|
14
webroot/lib/Controller/Sql.php
Normal file
14
webroot/lib/Controller/Sql.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
require_once __DIR__ . '/../../config.php';
|
||||
|
||||
class Sql
|
||||
{
|
||||
public static function connection(): \PDO
|
||||
{
|
||||
return new \PDO(CONFIG_SQL_DSN, CONFIG_SQL_USER, CONFIG_SQL_PASSWORD);
|
||||
}
|
||||
}
|
51
webroot/lib/Controller/Transaction.php
Normal file
51
webroot/lib/Controller/Transaction.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\Context;
|
||||
use Model\User;
|
||||
|
||||
class Transaction
|
||||
{
|
||||
protected function __construct(
|
||||
protected Context $context,
|
||||
protected \PDO $sql,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function run(Context $context, callable $f): mixed
|
||||
{
|
||||
$sql = Sql::connection();
|
||||
$sql->query('START TRANSACTION');
|
||||
$result = $f(new self($context, $sql));
|
||||
$sql->query('COMMIT');
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function createBooking(User $user, int $type, int $amount, ?User $related, string $text): bool
|
||||
{
|
||||
$stmt = $this->sql->prepare('SELECT balance FROM user WHERE id = ?');
|
||||
$stmt->execute([$user->id]);
|
||||
$currentAmount = (int) $stmt->fetch(\PDO::FETCH_ASSOC)['balance'];
|
||||
$newAmount = (int) ($currentAmount + $amount);
|
||||
if ($newAmount < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// create booking entry
|
||||
$stmt = $this->sql->prepare(
|
||||
'INSERT INTO booking
|
||||
(affected, time, type, amount, related, comment)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$user->id, $this->context->requestTime, $type, $amount, $related?->id, $text]);
|
||||
|
||||
// change amount
|
||||
$stmt = $this->sql->prepare('UPDATE user SET balance = ? WHERE id = ?');
|
||||
$stmt->execute([$newAmount, $user->id]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
76
webroot/lib/Controller/TransferController.php
Normal file
76
webroot/lib/Controller/TransferController.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Model\User;
|
||||
use View\TransferPage;
|
||||
use View\MoneyFormatter;
|
||||
use View\Sendable;
|
||||
|
||||
class TransferController extends RestrictedPageController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('/transfer.php');
|
||||
}
|
||||
|
||||
protected function runLogic(): Sendable
|
||||
{
|
||||
$transferPage = new TransferPage($this->context);
|
||||
if ($transferPage->formWasSent) {
|
||||
$transferPage->fieldTarget = trim($transferPage->fieldTarget);
|
||||
$error = false;
|
||||
|
||||
// find target user
|
||||
$targetUser = User::byName($transferPage->fieldTarget);
|
||||
if (empty($targetUser)) {
|
||||
$transferPage->errorTargetNotFound = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check amount
|
||||
$amount = MoneyFormatter::parseAmount($transferPage->fieldAmount);
|
||||
if ($amount === null) {
|
||||
$transferPage->errorAmountInvalid = true;
|
||||
$error = true;
|
||||
} elseif ($amount === 0) {
|
||||
$transferPage->errorAmountZero = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// check text
|
||||
$text = $transferPage->fieldText;
|
||||
if (iconv_strlen($text) > 100) {
|
||||
$transferPage->errorTextTooLong = true;
|
||||
$error = true;
|
||||
}
|
||||
|
||||
// create the actual bookings
|
||||
if (!$error) {
|
||||
$success = Transaction::run($this->context, function ($transaction) use ($targetUser, $amount, $text) {
|
||||
$success = $transaction->createBooking($this->context->session->user, 3, -$amount, $targetUser, $text);
|
||||
if ($success) {
|
||||
$transaction->createBooking($targetUser, 4, $amount, $this->context->session->user, $text);
|
||||
}
|
||||
return $success;
|
||||
});
|
||||
if (!$success) {
|
||||
$transferPage->errorInsufficientFunds = true;
|
||||
$error = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$error) {
|
||||
$transferPage->fieldTarget = '';
|
||||
$transferPage->fieldAmount = '';
|
||||
$transferPage->fieldText = '';
|
||||
|
||||
$transferPage->success = true;
|
||||
$transferPage->successTarget = $targetUser->name;
|
||||
$transferPage->successAmount = $amount;
|
||||
}
|
||||
}
|
||||
return $transferPage;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user