From 2133574caebcc8aa6748158148f9fe26f5946e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20F=C3=BCrderer?= Date: Sat, 1 Jun 2024 15:13:50 +0200 Subject: [PATCH] Create an insecure bank application --- .gitignore | 1 + README.md | 9 ++ db-init.sql | 33 ++++++ webroot/autoload.php | 7 ++ webroot/bookings.php | 8 ++ webroot/config.sample.php | 6 + webroot/deposit.php | 8 ++ webroot/index.php | 9 ++ .../Controller/BookingOverviewController.php | 41 +++++++ .../Controller/CashTransactionController.php | 78 +++++++++++++ webroot/lib/Controller/LoginController.php | 43 +++++++ webroot/lib/Controller/LogoutController.php | 21 ++++ webroot/lib/Controller/RegisterController.php | 61 ++++++++++ .../Controller/RestrictedPageController.php | 38 +++++++ webroot/lib/Controller/Sql.php | 14 +++ webroot/lib/Controller/Transaction.php | 51 +++++++++ webroot/lib/Controller/TransferController.php | 76 +++++++++++++ webroot/lib/Model/Context.php | 40 +++++++ webroot/lib/Model/NavigationEntry.php | 24 ++++ webroot/lib/Model/Session.php | 59 ++++++++++ webroot/lib/Model/User.php | 46 ++++++++ webroot/lib/View/AccessDeniedPage.php | 23 ++++ webroot/lib/View/BankingPage.php | 32 ++++++ webroot/lib/View/BookingOverviewPage.php | 60 ++++++++++ webroot/lib/View/CashTransactionPage.php | 106 ++++++++++++++++++ webroot/lib/View/FrontPage.php | 28 +++++ webroot/lib/View/Html.php | 30 +++++ webroot/lib/View/LoginPage.php | 44 ++++++++ webroot/lib/View/LoginRedirection.php | 21 ++++ webroot/lib/View/LogoutRedirection.php | 16 +++ webroot/lib/View/MoneyFormatter.php | 30 +++++ webroot/lib/View/RegisterPage.php | 77 +++++++++++++ webroot/lib/View/Sendable.php | 9 ++ webroot/lib/View/TransferPage.php | 89 +++++++++++++++ webroot/login.php | 8 ++ webroot/logout.php | 8 ++ webroot/register.php | 8 ++ webroot/style.css | 92 +++++++++++++++ webroot/transfer.php | 8 ++ webroot/withdraw.php | 8 ++ 40 files changed, 1370 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 db-init.sql create mode 100644 webroot/autoload.php create mode 100644 webroot/bookings.php create mode 100644 webroot/config.sample.php create mode 100644 webroot/deposit.php create mode 100644 webroot/index.php create mode 100644 webroot/lib/Controller/BookingOverviewController.php create mode 100644 webroot/lib/Controller/CashTransactionController.php create mode 100644 webroot/lib/Controller/LoginController.php create mode 100644 webroot/lib/Controller/LogoutController.php create mode 100644 webroot/lib/Controller/RegisterController.php create mode 100644 webroot/lib/Controller/RestrictedPageController.php create mode 100644 webroot/lib/Controller/Sql.php create mode 100644 webroot/lib/Controller/Transaction.php create mode 100644 webroot/lib/Controller/TransferController.php create mode 100644 webroot/lib/Model/Context.php create mode 100644 webroot/lib/Model/NavigationEntry.php create mode 100644 webroot/lib/Model/Session.php create mode 100644 webroot/lib/Model/User.php create mode 100644 webroot/lib/View/AccessDeniedPage.php create mode 100644 webroot/lib/View/BankingPage.php create mode 100644 webroot/lib/View/BookingOverviewPage.php create mode 100644 webroot/lib/View/CashTransactionPage.php create mode 100644 webroot/lib/View/FrontPage.php create mode 100644 webroot/lib/View/Html.php create mode 100644 webroot/lib/View/LoginPage.php create mode 100644 webroot/lib/View/LoginRedirection.php create mode 100644 webroot/lib/View/LogoutRedirection.php create mode 100644 webroot/lib/View/MoneyFormatter.php create mode 100644 webroot/lib/View/RegisterPage.php create mode 100644 webroot/lib/View/Sendable.php create mode 100644 webroot/lib/View/TransferPage.php create mode 100644 webroot/login.php create mode 100644 webroot/logout.php create mode 100644 webroot/register.php create mode 100644 webroot/style.css create mode 100644 webroot/transfer.php create mode 100644 webroot/withdraw.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c0151f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/webroot/config.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..25f38a9 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Setup + +- Use a typical Webserver + PHP + SQL setup +- Initialize the database with the instructions from `db-init.sql` +- Copy the `webroot` directory onto the webserver +- Inside `webroot`, copy `config.sample.php` to `config.php` and enter the SQL credentials +- Register on the webpage to get your own account +- Make yourself an admin: + Using an SQL management software, set the `admin` field to `1` in the entry of the table `user` that corresponds to your account. diff --git a/db-init.sql b/db-init.sql new file mode 100644 index 0000000..43edc97 --- /dev/null +++ b/db-init.sql @@ -0,0 +1,33 @@ +CREATE TABLE `user` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `password` varchar(255) NOT NULL, + `admin` bit(1) NOT NULL DEFAULT b'0', + `balance` bigint(20) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + + +CREATE TABLE `session` ( + `token` binary(32) NOT NULL, + `user` int(10) unsigned NOT NULL, + KEY `user` (`user`), + CONSTRAINT `session_ibfk_1` FOREIGN KEY (`user`) REFERENCES `user` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; + + +CREATE TABLE `booking` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `affected` int(10) unsigned NOT NULL, + `time` bigint(20) NOT NULL, + `type` tinyint(1) unsigned NOT NULL, + `amount` bigint(20) NOT NULL, + `related` int(10) unsigned DEFAULT NULL, + `comment` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `affected` (`affected`), + KEY `related` (`related`), + CONSTRAINT `booking_ibfk_1` FOREIGN KEY (`affected`) REFERENCES `user` (`id`) ON DELETE CASCADE, + CONSTRAINT `booking_ibfk_2` FOREIGN KEY (`related`) REFERENCES `user` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/webroot/autoload.php b/webroot/autoload.php new file mode 100644 index 0000000..b137294 --- /dev/null +++ b/webroot/autoload.php @@ -0,0 +1,7 @@ +run()->send(); diff --git a/webroot/config.sample.php b/webroot/config.sample.php new file mode 100644 index 0000000..16b6bcd --- /dev/null +++ b/webroot/config.sample.php @@ -0,0 +1,6 @@ +run()->send(); diff --git a/webroot/index.php b/webroot/index.php new file mode 100644 index 0000000..6cd2e44 --- /dev/null +++ b/webroot/index.php @@ -0,0 +1,9 @@ +send(); diff --git a/webroot/lib/Controller/BookingOverviewController.php b/webroot/lib/Controller/BookingOverviewController.php new file mode 100644 index 0000000..da0ef78 --- /dev/null +++ b/webroot/lib/Controller/BookingOverviewController.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/webroot/lib/Controller/CashTransactionController.php b/webroot/lib/Controller/CashTransactionController.php new file mode 100644 index 0000000..f97358d --- /dev/null +++ b/webroot/lib/Controller/CashTransactionController.php @@ -0,0 +1,78 @@ +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; + } +} diff --git a/webroot/lib/Controller/LoginController.php b/webroot/lib/Controller/LoginController.php new file mode 100644 index 0000000..e54af13 --- /dev/null +++ b/webroot/lib/Controller/LoginController.php @@ -0,0 +1,43 @@ +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; + } +} diff --git a/webroot/lib/Controller/LogoutController.php b/webroot/lib/Controller/LogoutController.php new file mode 100644 index 0000000..e301525 --- /dev/null +++ b/webroot/lib/Controller/LogoutController.php @@ -0,0 +1,21 @@ +context->session->destroy(); + return new LogoutRedirection(); + } +} diff --git a/webroot/lib/Controller/RegisterController.php b/webroot/lib/Controller/RegisterController.php new file mode 100644 index 0000000..655de48 --- /dev/null +++ b/webroot/lib/Controller/RegisterController.php @@ -0,0 +1,61 @@ +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; + } + } +} diff --git a/webroot/lib/Controller/RestrictedPageController.php b/webroot/lib/Controller/RestrictedPageController.php new file mode 100644 index 0000000..9c813f0 --- /dev/null +++ b/webroot/lib/Controller/RestrictedPageController.php @@ -0,0 +1,38 @@ +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; +} diff --git a/webroot/lib/Controller/Sql.php b/webroot/lib/Controller/Sql.php new file mode 100644 index 0000000..fdbf80c --- /dev/null +++ b/webroot/lib/Controller/Sql.php @@ -0,0 +1,14 @@ +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; + } +} diff --git a/webroot/lib/Controller/TransferController.php b/webroot/lib/Controller/TransferController.php new file mode 100644 index 0000000..bb6ee81 --- /dev/null +++ b/webroot/lib/Controller/TransferController.php @@ -0,0 +1,76 @@ +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; + } +} diff --git a/webroot/lib/Model/Context.php b/webroot/lib/Model/Context.php new file mode 100644 index 0000000..6a4d68c --- /dev/null +++ b/webroot/lib/Model/Context.php @@ -0,0 +1,40 @@ +navigation = []; + $this->navigation[] = new NavigationEntry('Startseite', '/'); + if ($this->session === null) { + $this->navigation[] = new NavigationEntry('Registrieren', '/register.php'); + $this->navigation[] = new NavigationEntry('Einloggen', '/login.php'); + } else { + $this->navigation[] = new NavigationEntry('Umsatzübersicht', '/bookings.php'); + if ($this->session->user->isAdmin) { + $this->navigation[] = new NavigationEntry('Einzahlen', '/deposit.php'); + $this->navigation[] = new NavigationEntry('Auszahlen', '/withdraw.php'); + } + $this->navigation[] = new NavigationEntry('Überweisen', '/transfer.php'); + $this->navigation[] = new NavigationEntry('Ausloggen', '/logout.php'); + } + } + + public static function init(string $url): self + { + $context = new self(); + $context->requestTime = time(); + $context->currentPage = $url; + $context->session = Session::load(); + $context->initNavigation(); + return $context; + } +} diff --git a/webroot/lib/Model/NavigationEntry.php b/webroot/lib/Model/NavigationEntry.php new file mode 100644 index 0000000..fce82f9 --- /dev/null +++ b/webroot/lib/Model/NavigationEntry.php @@ -0,0 +1,24 @@ +displayText); + $url = htmlspecialchars($this->url); + if ($this->url === $context->currentPage) { + echo "
  • {$displayText}
  • "; + } else { + echo "
  • {$displayText}
  • "; + } + } +} diff --git a/webroot/lib/Model/Session.php b/webroot/lib/Model/Session.php new file mode 100644 index 0000000..8082704 --- /dev/null +++ b/webroot/lib/Model/Session.php @@ -0,0 +1,59 @@ +prepare('INSERT INTO session (token, user) VALUES (UNHEX(?), ?)'); + $stmt->execute([$sessidHash, $user->id]); + + $session = new self(); + $session->newSessid = $sessid; + $session->user = $user; + return $session; + } + + public static function load(): ?self + { + if (!isset($_COOKIE['sessid'])) { + return null; + } + $sessidHash = hash('sha256', $_COOKIE['sessid']); + + $sql = Sql::connection(); + $stmt = $sql->prepare( + 'SELECT user.id, user.name, user.admin FROM session + JOIN user ON session.user = user.id + WHERE token = UNHEX(?)' + ); + $stmt->execute([$sessidHash]); + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $session = new Session(); + $session->tokenHash = $sessidHash; + $session->user = new User($row['id'], $row['name'], null, (bool) $row['admin']); + return $session; + } + + return null; + } + + public function destroy(): void + { + $sql = Sql::connection(); + $stmt = $sql->prepare('DELETE FROM session WHERE token = UNHEX(?)'); + $stmt->execute([$this->tokenHash]); + } +} diff --git a/webroot/lib/Model/User.php b/webroot/lib/Model/User.php new file mode 100644 index 0000000..c0ea0e2 --- /dev/null +++ b/webroot/lib/Model/User.php @@ -0,0 +1,46 @@ +prepare('INSERT INTO user (name, password) VALUES (?, ?)'); + try { + $stmt->execute([$name, $pwHash]); + } catch (\PDOException $e) { + if ($e->getCode() == 23000) { + // duplicate entry, so the username is already in use + return null; + } else { + // unknown error, throw out + throw $e; + } + } + return new self((int) $sql->lastInsertId(), $name, $pwHash, false); + } + + public static function byName(string $name): ?self + { + $sql = Sql::connection(); + $stmt = $sql->prepare('SELECT id, name, password, admin FROM user WHERE name = ?'); + $stmt->execute([$name]); + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return new self($row['id'], $row['name'], $row['password'], (bool) $row['admin']); + } + return null; + } +} diff --git a/webroot/lib/View/AccessDeniedPage.php b/webroot/lib/View/AccessDeniedPage.php new file mode 100644 index 0000000..37c33f4 --- /dev/null +++ b/webroot/lib/View/AccessDeniedPage.php @@ -0,0 +1,23 @@ +Zugriff Verweigert'; + echo '

    Bitte wähle eine andere Seite in der Navigation aus.

    '; + } +} diff --git a/webroot/lib/View/BankingPage.php b/webroot/lib/View/BankingPage.php new file mode 100644 index 0000000..72cbeab --- /dev/null +++ b/webroot/lib/View/BankingPage.php @@ -0,0 +1,32 @@ +'; + } + + public function sendBody(): void + { + echo '
    '; + $this->sendMainContent(); + echo '
    '; + } + + abstract public function sendMainContent(): void; +} diff --git a/webroot/lib/View/BookingOverviewPage.php b/webroot/lib/View/BookingOverviewPage.php new file mode 100644 index 0000000..65e0676 --- /dev/null +++ b/webroot/lib/View/BookingOverviewPage.php @@ -0,0 +1,60 @@ +context->session->user->name); + + echo "

    Umsatz-Übersicht für {$userName}

    "; + if (empty($this->bookings)) { + echo '

    Keine Buchungen

    '; + } + echo ''; + foreach ($this->bookings as $booking) { + $timeInfo = date('d.m.Y H:i', $booking['time']); + $text = htmlspecialchars(static::bookingText($booking['type'], $booking['relatedName'])); + $comment = htmlspecialchars($booking['comment']); + $amount = MoneyFormatter::formatAmount($booking['amount']); + echo ""; + } + $final = MoneyFormatter::formatAmount($this->finalBalance); + echo ""; + echo '
    {$timeInfo}{$text}{$comment}{$amount}
    Kontostand:{$final}
    '; + } +} diff --git a/webroot/lib/View/CashTransactionPage.php b/webroot/lib/View/CashTransactionPage.php new file mode 100644 index 0000000..5435ff0 --- /dev/null +++ b/webroot/lib/View/CashTransactionPage.php @@ -0,0 +1,106 @@ +currentPage) { + case '/deposit.php': + $this->title = 'Einzahlen'; + break; + case '/withdraw.php': + $this->title = 'Auszahlen'; + break; + } + + if (isset($_POST['customer'], $_POST['amount'], $_POST['text'])) { + $this->formWasSent = true; + $this->fieldCustomer = (string) $_POST['customer']; + $this->fieldAmount = (string) $_POST['amount']; + $this->fieldText = (string) $_POST['text']; + } + } + + public function renderErrors(): string + { + $errors = []; + if ($this->errorCustomerNotFound) { + $errors[] = '[!] Der angegebene Kunde konnte nicht gefunden werden.'; + } + if ($this->errorAmountInvalid) { + $errors[] = '[!] Der eingegebene Betrag entspricht nicht dem vorgesehenen Format.'; + } + if ($this->errorAmountZero) { + $errors[] = '[!] Der Betrag muss größer als 0,00 € sein.'; + } + if ($this->errorInsufficientFunds) { + $errors[] = '[!] Das Konto des Kunden ist nicht ausreichend gedeckt.'; + } + if ($this->errorTextTooLong) { + $errors[] = '[!] Der Buchungstext darf nicht länger als 100 Zeichen sein.'; + } + return implode('
    ', $errors); + } + + public function sendTitle(): void + { + echo $this->title; + } + + public function sendMainContent(): void + { + $customer = htmlspecialchars($this->fieldCustomer); + $amount = htmlspecialchars($this->fieldAmount); + $text = htmlspecialchars($this->fieldText); + + echo "

    {$this->title}

    "; + + $errors = $this->renderErrors(); + if (!empty($errors)) { + echo "

    {$errors}

    "; + } + + if ($this->success) { + $successCustomer = htmlspecialchars($this->successCustomer); + $successAmount = MoneyFormatter::formatAmount($this->successAmount); + switch ($this->context->currentPage) { + case '/deposit.php': + echo "

    Es wurden {$successAmount} auf das Konto von {$successCustomer} eingezahlt.

    "; + break; + case '/withdraw.php': + echo "

    Es wurden {$successAmount} aus dem Konto von {$successCustomer} ausgezahlt.

    "; + break; + } + } + + echo "
    context->currentPage}\" method=\"post\">"; + echo "
    "; + echo "
    "; + echo "
    "; + echo "title}\">"; + echo '
    '; + } +} diff --git a/webroot/lib/View/FrontPage.php b/webroot/lib/View/FrontPage.php new file mode 100644 index 0000000..4c1e31e --- /dev/null +++ b/webroot/lib/View/FrontPage.php @@ -0,0 +1,28 @@ +Online-Banking Instanz mit Sicherheitslücken'; + echo '

    In dieser Applikation gibt es mindestens zwei Sicherheitslücken. Kannst du sie ausnutzen?

    '; + echo '

    Ablauf:

      '; + echo '
    1. Registriere dich über das entsprechende Formular.
    2. '; + echo '
    3. Nenne mir deinen Nutzernamen.
    4. '; + echo '
    5. Du erhältst 1000,00 € Startkapital.
    6. '; + echo '
    7. Nutze mindestens eine Sicherheitslücke aus und erschleiche 1 Million € oder mehr.
    8. '; + echo '
    '; + echo '

    Die beiden beabsichtigten Lücken wurden jeweils in einem kurzen Text-Dokument beschrieben. Die SHA256-Hashwerte dieser Dokumente lauten:

    '; + echo 'bb86334c2c4ecb4c3f35c35392a3867824216f0e23bee5c7b60953e41d7b7590 sicherheitsluecke-1.txt
    '; + echo '75dde975461d9d20c81aa10a42ae886820bead9201bc032b16ce00dba873d89a sicherheitsluecke-2.txt

    '; + echo 'Damit kann später zweifelsfrei überprüft werden, ob eine gefundene Sicherheitslücke tatsächlich beabsichtigt war oder ob es sich um eine weitere, unbeabsichtigte Lücke handelt.

    '; + } +} diff --git a/webroot/lib/View/Html.php b/webroot/lib/View/Html.php new file mode 100644 index 0000000..2bea363 --- /dev/null +++ b/webroot/lib/View/Html.php @@ -0,0 +1,30 @@ +sendHeader(); + echo ''; + $this->sendHead(); + $this->sendBody(); + } + + public function sendHeader(): void + { + } + + public function sendHead(): void + { + echo ''; + $this->sendTitle(); + echo ''; + } + + abstract public function sendTitle(): void; + + abstract public function sendBody(): void; +} \ No newline at end of file diff --git a/webroot/lib/View/LoginPage.php b/webroot/lib/View/LoginPage.php new file mode 100644 index 0000000..cdb3782 --- /dev/null +++ b/webroot/lib/View/LoginPage.php @@ -0,0 +1,44 @@ +formWasSent = true; + $this->fieldUsername = (string) $_POST['username']; + $this->fieldPassword = (string) $_POST['password']; + } + } + + public function sendTitle(): void + { + echo 'Einloggen'; + } + + public function sendMainContent(): void + { + $username = htmlspecialchars($this->fieldUsername); + $password = htmlspecialchars($this->fieldPassword); + + echo '

    Einloggen

    '; + if ($this->errorLoginDataInvalid) { + echo '

    [!] Der Login war nicht erfolgreich.

    '; + } + echo '
    '; + echo "
    "; + echo "
    "; + echo ''; + echo '
    '; + } +} diff --git a/webroot/lib/View/LoginRedirection.php b/webroot/lib/View/LoginRedirection.php new file mode 100644 index 0000000..dbeadb4 --- /dev/null +++ b/webroot/lib/View/LoginRedirection.php @@ -0,0 +1,21 @@ +context->session->newSessid; + http_response_code(303); // "see other" redirection + setcookie('sessid', $sessid, time() + 86400 * 365); // session creation + header('Location: /bookings.php'); // login redirection + } +} diff --git a/webroot/lib/View/LogoutRedirection.php b/webroot/lib/View/LogoutRedirection.php new file mode 100644 index 0000000..6ac76f6 --- /dev/null +++ b/webroot/lib/View/LogoutRedirection.php @@ -0,0 +1,16 @@ +- {$inner}"; + } + $euro = intdiv($amount, 100); + $cent = sprintf("%02d", $amount % 100); + return "{$euro},{$cent} €"; + } + + public static function parseAmount(string $amount): ?int + { + $pattern = '/^([0-9]+)(,([0-9]{2}))?$/'; + if (preg_match($pattern, $amount, $matches)) { + $euro = (int) $matches[1]; + $cent = (int) ($matches[3] ?? 0); + $amount = 100 * $euro + $cent; + return (int) $amount; + } + return null; + } +} diff --git a/webroot/lib/View/RegisterPage.php b/webroot/lib/View/RegisterPage.php new file mode 100644 index 0000000..47d4163 --- /dev/null +++ b/webroot/lib/View/RegisterPage.php @@ -0,0 +1,77 @@ +formWasSent = true; + $this->fieldUsername = (string) $_POST['username']; + $this->fieldPassword = (string) $_POST['password']; + $this->fieldRepeatPassword = (string) $_POST['repeat_password']; + } + } + + public function sendTitle(): void + { + echo 'Registrieren'; + } + + public function renderErrors(): string + { + $errors = []; + if ($this->errorUsernameEmpty) { + $errors[] = '[!] Bitte wähle einen Nutzernamen.'; + } + if ($this->errorUsernameTooLong) { + $errors[] = '[!] Der Nutzername darf nicht länger als 20 Zeichen sein.'; + } + if ($this->errorUsernameInUse) { + $errors[] = '[!] Der Nutzername wird bereits von einem Account verwendet.'; + } + if ($this->errorPasswordEmpty) { + $errors[] = '[!] Bitte wähle ein Passwort.'; + } + if ($this->errorPasswordsMismatch) { + $errors[] = '[!] Die beiden Passwörter stimmen nicht überein.'; + } + return implode('
    ', $errors); + } + + public function sendMainContent(): void + { + $username = htmlspecialchars($this->fieldUsername); + $password = htmlspecialchars($this->fieldPassword); + $repeatPassword = htmlspecialchars($this->fieldRepeatPassword); + + echo '

    Registrieren

    '; + echo '

    Erstelle dir hier einen neuen Banking-Account.

    '; + $errors = $this->renderErrors(); + if (!empty($errors)) { + echo "

    {$errors}

    "; + } + echo '
    '; + echo "
    "; + echo "
    "; + echo "
    "; + echo ''; + echo '
    '; + } +} diff --git a/webroot/lib/View/Sendable.php b/webroot/lib/View/Sendable.php new file mode 100644 index 0000000..090af05 --- /dev/null +++ b/webroot/lib/View/Sendable.php @@ -0,0 +1,9 @@ +formWasSent = true; + $this->fieldTarget = (string) $_POST['target']; + $this->fieldAmount = (string) $_POST['amount']; + $this->fieldText = (string) $_POST['text']; + } + } + + public function renderErrors(): string + { + $errors = []; + if ($this->errorTargetNotFound) { + $errors[] = '[!] Der angegebene Nutzername (Zielkonto) konnte nicht gefunden werden.'; + } + if ($this->errorAmountInvalid) { + $errors[] = '[!] Der eingegebene Betrag entspricht nicht dem vorgesehenen Format.'; + } + if ($this->errorAmountZero) { + $errors[] = '[!] Der Betrag muss größer als 0,00 € sein.'; + } + if ($this->errorInsufficientFunds) { + $errors[] = '[!] Dein Konto ist nicht ausreichend gedeckt.'; + } + if ($this->errorTextTooLong) { + $errors[] = '[!] Der Buchungstext darf nicht länger als 100 Zeichen sein.'; + } + return implode('
    ', $errors); + } + + public function sendTitle(): void + { + echo 'Überweisen'; + } + + public function sendMainContent(): void + { + $target = htmlspecialchars($this->fieldTarget); + $amount = htmlspecialchars($this->fieldAmount); + $text = htmlspecialchars($this->fieldText); + + echo "

    Überweisen

    "; + + $errors = $this->renderErrors(); + if (!empty($errors)) { + echo "

    {$errors}

    "; + } + + if ($this->success) { + $successTarget = htmlspecialchars($this->successTarget); + $successAmount = MoneyFormatter::formatAmount($this->successAmount); + echo "

    Es wurden {$successAmount} an {$successTarget} überwiesen.

    "; + } + + echo "
    context->currentPage}\" method=\"post\">"; + echo "
    "; + echo "
    "; + echo "
    "; + echo ""; + echo '
    '; + } +} diff --git a/webroot/login.php b/webroot/login.php new file mode 100644 index 0000000..e36d7d7 --- /dev/null +++ b/webroot/login.php @@ -0,0 +1,8 @@ +run()->send(); diff --git a/webroot/logout.php b/webroot/logout.php new file mode 100644 index 0000000..68171df --- /dev/null +++ b/webroot/logout.php @@ -0,0 +1,8 @@ +run()->send(); diff --git a/webroot/register.php b/webroot/register.php new file mode 100644 index 0000000..de93438 --- /dev/null +++ b/webroot/register.php @@ -0,0 +1,8 @@ +run()->send(); diff --git a/webroot/style.css b/webroot/style.css new file mode 100644 index 0000000..142592c --- /dev/null +++ b/webroot/style.css @@ -0,0 +1,92 @@ +html, body { + font-family: monospace; + font-size: 1rem; + margin: 0; + padding: 0; +} +main { + margin: 2rem; +} +nav ul { + display: flex; + margin: 0; + padding: 0; +} +nav li { + flex: 1 1 0; + list-style-type: none; + margin: 0; + padding: 0; +} +nav a, nav span { + border-right: 2px solid #aaa; + border-bottom: 2px solid #aaa; + display: block; + padding: 1.5rem; + text-align: center; + text-decoration: none; +} +nav a { + color: #000; +} +nav a:hover, nav li[aria-current="page"] { + background-color: #000; + color: #fff; +} +nav li:last-child, nav li:last-child a { + border-right: none; +} +input { + background-color: #fff; + border: 1px solid #000; + box-sizing: border-box; + font-family: monospace; + font-size: 1rem; + margin: 1rem 0; + padding: 0.5rem; + width: 30rem; +} +.register input[type="submit"], .transfer input[type="submit"] { + width: 50rem; +} +.login input[type="submit"], .cash-transaction input[type="submit"] { + width: 45rem; +} +label { + display: inline-block; +} +.register label, .transfer label { + width: 20rem; +} +.login label, .cash-transaction label { + width: 15rem; +} +p.error { + color: #a00; + font-weight: bold; +} +p.success { + color: #080; + font-weight: bold; +} +.negative { + color: #800; +} +table { + border-spacing: 0; +} +td { + padding: 0 1rem; +} +tr:nth-child(2n) { + background-color: #eee; +} +tr:last-child { + font-weight: bold; +} +td:last-child { + text-align: right; +} +.pre { + white-space: pre-wrap; +} diff --git a/webroot/transfer.php b/webroot/transfer.php new file mode 100644 index 0000000..c379ee2 --- /dev/null +++ b/webroot/transfer.php @@ -0,0 +1,8 @@ +run()->send(); diff --git a/webroot/withdraw.php b/webroot/withdraw.php new file mode 100644 index 0000000..1314c7c --- /dev/null +++ b/webroot/withdraw.php @@ -0,0 +1,8 @@ +run()->send();