Login-System (PHP und AJAX)

Erstellt am: Wednesday 01 February 2017  |  Letzte Änderung am: Monday 09 July 2018

Übertragung von vertraulichen Daten per HTTP-Protokoll ist unsicher, da alle Daten im Klartext durch das Internet geschickt werden. Heute möchte ich mir das Thema HTTP-Authentifizierung trotzdem vornehmen und ein wenig Gedanken machen, wie man so ein unsicheres Verfahren, zumindest etwas sicherer gestalten könnte.

Eine Beispielanwendung, welche ich im Laufe dieses Artikels aufbauen möchte, basiert auf PHP und JavaScript, zum übertragen von Benutzerdaten verwende ich HTTP-Protokoll. Das Kennwort wandert dabei, bei eingeschaltetem Javascript, vom Client aus verschlüsselt über das Netz. Der Passwort-Hash ist von Anfrage zu Anfrage unterschiedlich und einmalig in der Anwendung, sodass ein Hash Hijacking keinen Sinn machen würde. Diese Methode bietet sicher keine echte Garantie und ist wohl eher als eine Art Experiment oder ein Versuch zu verstehen, um einen Login-Vorgang über HTTP etwas zu verbessern.

Beachte auch, wenn eine hohe Anforderung an die Sicherheit der Authentifizierung besteht, sollte die Übertragung immer durch die Verwendung von SSL/TLS abgesichert werden. HTTPS-Kommunikationsprotokoll wird im World Wide Web benutzt, um Daten abhörsicher zu übertragen und ist in dem Fall mit Sicherheit eine weitaus bessere Alternative.

Wer die in diesem Artikel beschriebene Skripte ausprobieren möchte, kann sich auf GitHub die Dateien herunterladen: https://github.com/bigin/login_php_js

Die Analyse

Während einer unverschlüsselten HTTP-Verbindung senden und empfangen der Client und der Server Nachrichten als leicht lesbare Textzeichen. Ein gewöhnlicher Login-Vorgang könnte zum Beispiel so ablaufen:

  1. Der Client sendet Authentifikationsdaten (Benutzername und Kennwort) an den Server.
  2. Server überprüft Benutzerangaben und vergibt die Zugangsberechtigung für geschützten Bereich an den jeweiligen Client.

Also, wenn ein Angreifer zwischen den beiden Kommunikationsendpunkten sitzt, kann er die Informationen unter Umständen empfangen und somit die geheimen Daten entlocken. Dies möchte ich jetzt ändern, indem ich die sensiblen Daten, vor dem Absenden bereits auf dem Client, entsprechend verarbeite (hashe). Dabei verwende ich als Eingabe für meine Hashfunktion eine einmalige Zeichenfolge, sogenanntes Salt:

myhash = sha1(sha1(Klartextpass) + Random Salt)

Sehen wir uns doch mal an, wie der Login-Vorgang nach meiner Änderung aussehen würde:

  1. Der Client fordert einen Zufallswert (Salt) vom Server an.
  2. Server erzeugt einen einmaligen Salt-Wert, speichert soeben erzeugten Salt-Wert in der Datenbank und übermittelt diesen an den Client weiter.
  3. Der Client "hasht" das Passwort mit dem Salt mittels JavaScript und übermittelt den daraus resultierenden Hash-Wert zurück an den Server.
  4. Der Server holt sich das Passwort des Benutzers aus der Datenbank, "hasht" diesen mit dem zuvor erzeugten Salt. Vergleicht das Resultat mit dem vom Client übermittelten Hash-Wert und vergibt die Zugangsberechtigung für den geschützten Bereich an den jeweiligen Benutzer.

Soweit die Theorie – nun ran an die Praxis ...

Der Aufbau

Zuerst erzeuge ich zwei Tabellen, die ich später zum Testen des Logins verwenden möchte. Die erste davon, ist eine auth Tabelle, diese enthält temporär die Daten die ich für die Authentifizierung des Benutzers brauche, also Spalten: id, salt, user_name und t_point.

  1. Die Spalte salt beinhaltet später meinen zufälligen und einmaligen Salt, das ich bei jedem Request erzeugen möchte.
  2. Spalte user_name enthält Anmeldename des Benutzers, identisch mit dem Benutzernamen aus der Tabelle users, die ich ebenfalls gleich erstellen werde.
  3. Spalte mit Bezeichnung t_point enthält einen Zeitstempel, damit wird diesem Datensatz eine eindeutige Zeit zuordnet. Im Script wird dann geprüft, wenn nach der Generierung des Zeitstempels mehr Zeit verstrichen ist als – sagen wir – eine Minute, verliert der Eintrag seine Gültigkeit und wird aus der Tabelle gelöscht.

Die zweite Tabelle users, enthält wichtigsten Benutzerdaten, die während einer Registrierung erforderlich sind, also: Benutzername, Kennwort ...

Damit ich das Script auch gleich mal testen kann, lege ich in meiner soeben frisch erstellten Tabelle auch noch einen neuen Benutzer coombo an.

Die Spalte user_pass beinhaltet einen SHA1 Wert: sha1('banana')

--
-- Tabellenstruktur für Tabelle `auth`
--

CREATE TABLE `auth` (
  `id` int(10) UNSIGNED NOT NULL,
  `salt` char(40) CHARACTER SET ascii NOT NULL,
  `user_name` varchar(25) DEFAULT NULL,
  `t_point` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- Tabellenstruktur für Tabelle `users`
--

CREATE TABLE `users` (
  `id` int(11) NOT NULL,
  `user_name` varchar(25) DEFAULT NULL,
  `user_pass` char(40) CHARACTER SET ascii NOT NULL,
  `last_login` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- Daten für Tabelle `users`
--

INSERT INTO `users` (`id`, `user_name`, `user_pass`, `last_login`) VALUES
(1, 'coombo', '250e77f12a5ab6972a0895d290c4792f0a326ea8', '2014-02-02 16:21:03');

--
-- Indizes der exportierten Tabellen
--

--
-- Indizes für die Tabelle `auth`
--
ALTER TABLE `auth`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `user_name` (`user_name`);

--
-- Indizes für die Tabelle `users`
--
ALTER TABLE `users`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `user_name` (`user_name`);

--
-- AUTO_INCREMENT für exportierte Tabellen
--

--
-- AUTO_INCREMENT für Tabelle `auth`
--
ALTER TABLE `auth`
  MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
--
-- AUTO_INCREMENT für Tabelle `users`
--
ALTER TABLE `users`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;

Dateien:

config.php Die Konfigurationsdatei, beinhaltet die Verbindungsdaten zur MySQL-Datenbank Benutzername, Passwort Tabellen-Bezeichnungen und andere Einstellungen
connect.php Stellt eine Datenbankverbindung her
getsalt.php Generiert und speichert einen neuen Salt-Wert.
checkuser.php Authentifikation-Script.
sha1.js SHA1 - Implementierung in JavaScript: https://github.com/chrisveness/crypto/blob/master/sha1.js
index.php Loginformular.

config.php:

<?php
define('MYSQL_HOST', 'localhost');
define('MYSQL_USER', '*********');
define('MYSQL_PASS', '**********');
define('MYSQL_DATABASE', 'your_db_name');
define('USERS_TABLE', 'users');
define('AUTH_TABLE', 'auth');
define('MAXIMUM_ATTEMPTS', 10);
define('MINIMUM_NAME', 3);
define('MAXIMUM_NAME', 25);
// minimum password length etc ...

connect.php:

<?php
try
{
    $db = new PDO('mysql:host='.MYSQL_HOST.';dbname='.MYSQL_DATABASE.';charset=utf8', MYSQL_USER, MYSQL_PASS);
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (Exception $e)
{
    echo $e->getMessage();
}

getsalt.php:

<?php
session_start();
include('config.php');
include('connect.php');
$name = isset($_GET['name']) ? $_GET['name'] : '';
function generateSalt()
{
    $salt = '';
    if(empty($salt))
    {
        $charset = array('a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r',
            's','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J',
            'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','Ö','Ü',
            '0','1','2','3','4','5','6','7','8','9','.','-','^','*','~','°','%','&',
            '(',']','[',')','§','?','!','#','|','$','>','<','+','_','{','}','´','¸');
        for($i = 0; $i < 16; $i++)
            $salt .= $charset[mt_rand(0, (count($charset)-1))];
    }
    return $salt;
}
function saveSalt($db, $salt, $name)
{
    // Delete expired authorizations
    $db->exec("DELETE FROM `".AUTH_TABLE."` WHERE (`t_point` < DATE_SUB(NOW(), INTERVAL 1 MINUTE))");
    // Check if user exists
    $stmt = $db->prepare("SELECT COUNT(*) FROM `".USERS_TABLE."` WHERE `user_name` = ?");
    try
    {
        $stmt->execute(array($name));
    } catch(PDOException $e)
    {
        return false;
    }
    $result = (int) $stmt->fetchColumn();
    if(!$result) return false;
    // Save new salt
    $stmt = $db->prepare("INSERT INTO
                                      `".AUTH_TABLE."` (`id`, `salt`, `user_name`)
                               VALUES
                                      ('', ?, ?)");
    try
    {
        $stmt->execute(array(sha1($salt), $name));
    } catch(PDOException $e)
    {
        return false;
    }
    $count = $stmt->rowCount();
    if(!$count) return false;
    return true;
}
$usalt = '';
$usalt = generateSalt();
saveSalt($db, $usalt, $name);
exit(json_encode(array('salt' => sha1($usalt))));

checkuser.php:

<?php
function checkUser($db)
{
    if(isset($_SESSION['attempt']) && $_SESSION['attempt'] >= MAXIMUM_ATTEMPTS)
        return false;
    if(!isset($_POST['name']) || empty($_POST['name']))
        return false;
    if(strlen($_POST['name']) < MINIMUM_NAME || strlen($_POST['name']) > MAXIMUM_NAME)
        return false;
    if(!isset($_COOKIE[session_name()]))
        return false;
    !isset($_SESSION['attempt']) ? $_SESSION['attempt'] = 1 : $_SESSION['attempt']++;
    $stmt = $db->prepare("SELECT
                                 `".AUTH_TABLE."`.`salt`, `".USERS_TABLE."`.`user_pass`
                             FROM
                                 `".AUTH_TABLE."`, `".USERS_TABLE."`
                            WHERE
                                 `".AUTH_TABLE."`.`user_name` = ?
                              AND
                                 `".USERS_TABLE."`.`user_name` = ?
                              AND
                                 (`".AUTH_TABLE."`.`t_point` > DATE_SUB(NOW(),INTERVAL 1 MINUTE))");
    try
    {
        $stmt->execute(array($_POST['name'], $_POST['name']));
    } catch(PDOException $e)
    {
        echo $e->getMessage();
        return false;
    }
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    if(empty($result))
        return false;
    $stmt = $db->prepare("DELETE FROM `".AUTH_TABLE."` WHERE `user_name` = ?");
    try
    {
        $stmt->execute(array($_POST['name']));
    } catch(PDOException $e)
    {
        echo $e->getMessage();
        return false;
    }
    $affected_rows = $stmt->rowCount();
    if(sha1($result['user_pass'].$result['salt']) != $_POST['hash'])
        return false;
    // Login successful
    $stmt = $db->prepare("UPDATE
                                 `".USERS_TABLE."`
                             SET
                                 `last_login`=NOW()
                           WHERE
                                 `user_name` = ?");
    try
    {
        $stmt->execute(array($_POST['name']));
    } catch(PDOException $e)
    {
        echo $e->getMessage();
        return false;
    }
    $_SESSION['auth'] = 1;
    unset($_SESSION['attempt']);
    return true;
}
function logout(){session_destroy();session_unset();unset($_SESSION);}

sha1.js:

https://github.com/chrisveness/crypto/blob/master/sha1.js

index.php:

<?php
session_start();
include('config.php');
include('connect.php');
include('checkuser.php');
$ret = 0;
if(isset($_POST['name']) && !empty($_POST['name']))
    $ret = checkUser($db);
if(isset($_GET['logout']))
    logout();
?>
<!DOCTYPE html>
<html lang="de">
    <head>
        <meta charset="utf-8">
        <title>Login Form</title>
        <style type="text/css">
        body{text-align:center;font-family:"Helvetica Neue",Helvetica,FreeSans,Arial,Verdana,sans-serif;font-size:100%;
            -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#555;}
        section{max-width:400px;margin:auto;text-align:left;}
        fieldset{margin:40px 0;padding:20px;font-weight:bold;border:solid 1px #8e8e8e;border-top-color:#a3a3a3;
            border-left-color:#a3a3a3;border-radius:4px 4px 4px 4px;background:#fbfbf3;box-sizing:border-box}
        label{display:block;cursor:pointer;width:250px;font-weight:normal;margin-top:10px;padding-bottom:10px;}
        legend{font-size:1.5em;color:#555;font-weight:400}
        input{width:100%;border-radius:4px 4px 4px 4px;padding:11px 5px 11px;border:solid 1px #dbdbdb;
            border-top-color:#8e8e8e;
            border-left-color:#8e8e8e;font-size:0.8em;box-sizing:border-box}
        input[type=submit]{margin-top:10px;background:#369988;border:solid 1px #06312b;border-top-color: #066a64;
            border-left-color:#06635d;font-weight:bold;font-size:1em;color:#fff;padding:11px 20px 11px 20px;
            display:block;margin: 0;}
        input[type=submit]:hover{cursor:pointer;background:#37a594;}
        .errmess {color:red;}
        </style>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
        <script language="JavaScript" type="text/javascript" src="sha1.js"></script>
    </head>
    <script>
    $(document).ready(function(){
        $("#send").click(function(){
            var name = $('form').find('input[name=name]');
            var pass = $('form').find('input[name=pass]');
            var hash = $('form').find('input[name=hash]');
            $.getJSON("getsalt.php?name=" + name.val(), function(data, status) {
                if(data.salt != undefined) {
                    var hashed = Sha1.hash(Sha1.hash(pass.val()) + data.salt.toString());
                    hash.val(hashed);
                    pass.val('');
                    $('form').submit();
                }
            });
            return false;
        });
    });
    </script>
<body>
    <!-- Protected Section -->
<?php if(isset($_SESSION['auth']) && $_SESSION['auth'] == 1): ?>
    <p>You are logged-in [ <a href="<?php echo htmlspecialchars($_SERVER['PHP_SELF']).'?logout=1'; ?>">Logout</a> ]</p>

    <!-- Login form Section -->
<?php else:
    if(!$ret && isset($_SESSION['attempt']) && $_SESSION['attempt'] >= MAXIMUM_ATTEMPTS)
        echo '<p class="errmess">'.
            'Error: No more attempts allowed!<br />Delete your cookies and try again ...</p>';
    else if(isset($_POST['pass']) && !$ret && isset($_SESSION['attempt']))
        echo '<p class="errmess">'.
            'Error: Invalid user name or password!</p>';
    ?>
    <noscript><p class="errmess">Your browser does not support JavaScript!</p></noscript>
    <section>
        <form id="loginform" name="login" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF']); ?>" method="POST">
            <fieldset>
                <legend>Login Form</legend>
                <p><label for="name">Username</label>
                    <input id="name" class="index" name="name" type="text">
                    <label for="pass">Password</label>
                    <input id="pass" class="index" name="pass" type="password">
                    <input id="hash" type="hidden" name="hash"></p>
                <p><input id="send" type="submit" name="send" value="Login"></p>
            </fieldset>
        </form>
    </section>
<?php endif; ?>
</body>
</html>

So, das war's auch schon, zum Testen einfach das Script herunterladen, und die index.php aufrufen.

Die Test-Zugangsdaten

  • Username: coombo
  • Password: banana

Eine Schwachstelle ungesalzene Passwort-Hashes

Eine große Schwachstelle hat diese Vorgehensweise dennoch: Da ich in meinem Beispiel dynamische Salt-Werte verwendet habe, wird das Passwort zwar als ein Hash-Wert in der Datenbank gespeichert, "gesalzen" ist es jedoch nicht. Deshalb empfehle ich diese Methode nur zu Testzwecken zu verwenden, oder die gespeicherten Passwort-Hashes zusätzlich zu "salzen".

Autor: Bigin  |  Tags:  PHPScriptsDevelopment