Create an insecure bank application

This commit is contained in:
2024-06-01 15:13:50 +02:00
commit 2133574cae
40 changed files with 1370 additions and 0 deletions

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace View;
class AccessDeniedPage extends BankingPage
{
public function sendHeader(): void
{
http_response_code(403);
}
public function sendTitle(): void
{
echo 'Zugriff Verweigert';
}
public function sendMainContent(): void
{
echo '<h1>Zugriff Verweigert</h1>';
echo '<p>Bitte wähle eine andere Seite in der Navigation aus.</p>';
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace View;
use Model\Context;
abstract class BankingPage extends Html
{
public function __construct(protected Context $context)
{
}
public function sendHead(): void
{
parent::sendHead();
echo '<link href="/style.css" rel="stylesheet">';
}
public function sendBody(): void
{
echo '<nav><ul>';
foreach ($this->context->navigation as $entry) {
$entry->send($this->context);
}
echo '</ul></nav><main>';
$this->sendMainContent();
echo '</main>';
}
abstract public function sendMainContent(): void;
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace View;
class BookingOverviewPage extends BankingPage
{
public array $bookings = [];
public int $finalBalance = 0;
public function sendTitle(): void
{
echo 'Umsatz-Übersicht';
}
protected static function bookingText(int $type, ?string $relatedName)
{
switch ($type) {
case 1:
return "Einzahlung";
case 2:
return "Auszahlung";
case 3:
if ($relatedName === null) {
return "Überweisung ins Leere";
} else {
return "Überweisung an {$relatedName}";
}
case 4:
if ($relatedName === null) {
return "Überweisung aus dem Nichts";
} else {
return "Überweisung von {$relatedName}";
}
default:
return "Unbekannter Vorgang";
}
}
public function sendMainContent(): void
{
$userName = htmlspecialchars($this->context->session->user->name);
echo "<h1>Umsatz-Übersicht für {$userName}</h1>";
if (empty($this->bookings)) {
echo '<p><i>Keine Buchungen</i></p>';
}
echo '<table>';
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 "<tr><td>{$timeInfo}</td><td>{$text}</td><td>{$comment}</td><td>{$amount}</td></tr>";
}
$final = MoneyFormatter::formatAmount($this->finalBalance);
echo "<tr><td></td><td>Kontostand:</td><td></td><td>{$final}</td></tr>";
echo '</table>';
}
}

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace View;
use Model\Context;
class CashTransactionPage extends BankingPage
{
protected string $title;
public string $fieldCustomer = '';
public string $fieldAmount = '';
public string $fieldText = '';
public bool $formWasSent = false;
public bool $errorCustomerNotFound = false;
public bool $errorAmountInvalid = false;
public bool $errorAmountZero = false;
public bool $errorInsufficientFunds = false;
public bool $errorTextTooLong = false;
public bool $success = false;
public ?string $successCustomer = null;
public ?int $successAmount = null;
public function __construct(protected Context $context)
{
parent::__construct($context);
switch ($context->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('<br>', $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 "<h1>{$this->title}</h1>";
$errors = $this->renderErrors();
if (!empty($errors)) {
echo "<p class=\"error\">{$errors}</p>";
}
if ($this->success) {
$successCustomer = htmlspecialchars($this->successCustomer);
$successAmount = MoneyFormatter::formatAmount($this->successAmount);
switch ($this->context->currentPage) {
case '/deposit.php':
echo "<p class=\"success\">Es wurden {$successAmount} auf das Konto von {$successCustomer} eingezahlt.</p>";
break;
case '/withdraw.php':
echo "<p class=\"success\">Es wurden {$successAmount} aus dem Konto von {$successCustomer} ausgezahlt.</p>";
break;
}
}
echo "<form class=\"cash-transaction\" action=\"{$this->context->currentPage}\" method=\"post\">";
echo "<label for=\"customer\">Kundenname:</label><input type=\"text\" name=\"customer\" id=\"customer\" maxlength=\"20\" value=\"{$customer}\"><br>";
echo "<label for=\"amount\">Betrag:</label><input type=\"text\" name=\"amount\" id=\"amount\" value=\"{$amount}\"><br>";
echo "<label for=\"text\">Buchungstext:</label><input type=\"text\" name=\"text\" id=\"text\" value=\"{$text}\"><br>";
echo "<input type=\"submit\" value=\"{$this->title}\">";
echo '</form>';
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace View;
class FrontPage extends BankingPage
{
public function sendTitle(): void
{
echo 'Online-Banking';
}
public function sendMainContent(): void
{
echo '<h1>Online-Banking Instanz mit Sicherheitslücken</h1>';
echo '<p>In dieser Applikation gibt es mindestens zwei Sicherheitslücken. Kannst du sie ausnutzen?</p>';
echo '<p>Ablauf:</p><ol>';
echo '<li>Registriere dich über das entsprechende Formular.</li>';
echo '<li>Nenne mir deinen Nutzernamen.</li>';
echo '<li>Du erhältst 1000,00 € Startkapital.</li>';
echo '<li>Nutze mindestens eine Sicherheitslücke aus und erschleiche 1 Million € oder mehr.</li>';
echo '</ol>';
echo '<p class="pre">Die beiden beabsichtigten Lücken wurden jeweils in einem kurzen Text-Dokument beschrieben. Die SHA256-Hashwerte dieser Dokumente lauten:<br><br>';
echo 'bb86334c2c4ecb4c3f35c35392a3867824216f0e23bee5c7b60953e41d7b7590 sicherheitsluecke-1.txt<br>';
echo '75dde975461d9d20c81aa10a42ae886820bead9201bc032b16ce00dba873d89a sicherheitsluecke-2.txt<br><br>';
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.</p>';
}
}

30
webroot/lib/View/Html.php Normal file
View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace View;
abstract class Html implements Sendable
{
public function send(): void
{
$this->sendHeader();
echo '<!doctype html>';
$this->sendHead();
$this->sendBody();
}
public function sendHeader(): void
{
}
public function sendHead(): void
{
echo '<meta charset="UTF-8"><title>';
$this->sendTitle();
echo '</title>';
}
abstract public function sendTitle(): void;
abstract public function sendBody(): void;
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace View;
class LoginPage extends BankingPage
{
public string $fieldUsername = '';
public string $fieldPassword = '';
public bool $formWasSent = false;
public bool $errorLoginDataInvalid = false;
public function __construct(...$args)
{
parent::__construct(...$args);
if (isset($_POST['username'], $_POST['password'])) {
$this->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 '<h1>Einloggen</h1>';
if ($this->errorLoginDataInvalid) {
echo '<p class="error">[!] Der Login war nicht erfolgreich.</p>';
}
echo '<form class="login" action="/login.php" method="post">';
echo "<label for=\"username\">Nutzername:</label><input type=\"text\" name=\"username\" id=\"username\" maxlength=\"20\" value=\"{$username}\"><br>";
echo "<label for=\"password\">Passwort:</label><input type=\"password\" name=\"password\" id=\"password\" value=\"{$password}\"><br>";
echo '<input type="submit" value="Einloggen">';
echo '</form>';
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace View;
use Model\Context;
class LoginRedirection implements Sendable
{
public function __construct(protected Context $context)
{
}
public function send(): void
{
$sessid = $this->context->session->newSessid;
http_response_code(303); // "see other" redirection
setcookie('sessid', $sessid, time() + 86400 * 365); // session creation
header('Location: /bookings.php'); // login redirection
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace View;
use Model\Context;
class LogoutRedirection implements Sendable
{
public function send(): void
{
http_response_code(303); // "see other" redirection
setcookie('sessid', '', 1); // delete cookie
header('Location: /'); // redirection to home
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace View;
class MoneyFormatter
{
public static function formatAmount(int $amount): string
{
if ($amount < 0) {
$inner = static::formatAmount(-$amount);
return "<span class=\"negative\">- {$inner}</span>";
}
$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;
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace View;
use Model\Context;
class RegisterPage extends BankingPage
{
public string $fieldUsername = '';
public string $fieldPassword = '';
public string $fieldRepeatPassword = '';
public bool $formWasSent = false;
public bool $errorUsernameEmpty = false;
public bool $errorUsernameTooLong = false;
public bool $errorUsernameInUse = false;
public bool $errorPasswordEmpty = false;
public bool $errorPasswordsMismatch = false;
public function __construct(...$args)
{
parent::__construct(...$args);
if (isset($_POST['username'], $_POST['password'], $_POST['repeat_password'])) {
$this->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('<br>', $errors);
}
public function sendMainContent(): void
{
$username = htmlspecialchars($this->fieldUsername);
$password = htmlspecialchars($this->fieldPassword);
$repeatPassword = htmlspecialchars($this->fieldRepeatPassword);
echo '<h1>Registrieren</h1>';
echo '<p>Erstelle dir hier einen neuen Banking-Account.</p>';
$errors = $this->renderErrors();
if (!empty($errors)) {
echo "<p class=\"error\">{$errors}</p>";
}
echo '<form class="register" action="/register.php" method="post">';
echo "<label for=\"username\">Nutzername:</label><input type=\"text\" name=\"username\" id=\"username\" maxlength=\"20\" value=\"{$username}\"><br>";
echo "<label for=\"password\">Passwort:</label><input type=\"password\" name=\"password\" id=\"password\" value=\"{$password}\"><br>";
echo "<label for=\"repeat_password\">Passwort wiederholen:</label><input type=\"password\" name=\"repeat_password\" id=\"repeat_password\" value=\"{$repeatPassword}\"><br>";
echo '<input type="submit" value="Jetzt registrieren">';
echo '</form>';
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace View;
interface Sendable
{
public function send(): void;
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace View;
use Model\Context;
class TransferPage extends BankingPage
{
public string $fieldTarget = '';
public string $fieldAmount = '';
public string $fieldText = '';
public bool $formWasSent = false;
public bool $errorTargetNotFound = false;
public bool $errorAmountInvalid = false;
public bool $errorAmountZero = false;
public bool $errorInsufficientFunds = false;
public bool $errorTextTooLong = false;
public bool $success = false;
public ?string $successTarget = null;
public ?int $successAmount = null;
public function __construct(protected Context $context)
{
parent::__construct($context);
if (isset($_POST['target'], $_POST['amount'], $_POST['text'])) {
$this->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('<br>', $errors);
}
public function sendTitle(): void
{
echo 'Überweisen';
}
public function sendMainContent(): void
{
$target = htmlspecialchars($this->fieldTarget);
$amount = htmlspecialchars($this->fieldAmount);
$text = htmlspecialchars($this->fieldText);
echo "<h1>Überweisen</h1>";
$errors = $this->renderErrors();
if (!empty($errors)) {
echo "<p class=\"error\">{$errors}</p>";
}
if ($this->success) {
$successTarget = htmlspecialchars($this->successTarget);
$successAmount = MoneyFormatter::formatAmount($this->successAmount);
echo "<p class=\"success\">Es wurden {$successAmount} an {$successTarget} überwiesen.</p>";
}
echo "<form class=\"transfer\" action=\"{$this->context->currentPage}\" method=\"post\">";
echo "<label for=\"target\">Zielkonto (Nutzername):</label><input type=\"text\" name=\"target\" id=\"target\" maxlength=\"20\" value=\"{$target}\"><br>";
echo "<label for=\"amount\">Betrag:</label><input type=\"text\" name=\"amount\" id=\"amount\" value=\"{$amount}\"><br>";
echo "<label for=\"text\">Buchungstext:</label><input type=\"text\" name=\"text\" id=\"text\" value=\"{$text}\"><br>";
echo "<input type=\"submit\" value=\"Überweisen\">";
echo '</form>';
}
}