Tntnet: Formular-Reloads verhindern

Standard

Es gibt mehrere Gründe dafür den Reload eines HTML-Formulars zu verhindern:

  • Man will vermeiden, dass durch einen Page-Reload der User versehentlich Daten mehrmals speichert und unter Umständen damit Duplikate anlegt. Das passiert typischerweise wenn Seiten sehr langsam reagieren und der Benutzer ungeduldig auf die F5-Taste drückt oder den Reload-Button des Browsers anklickt.
  • Ein anderes Szenario wäre, dass der User in verschiedenen Browser-Fenstern das selbe Formular geöffnet hat. Dadurch könnte der User ungewollt seine zuvor im ersten Fenster gespeicherten Daten mit den auf dem zweiten Fenster überschreiben. Oder die Datenänderungen beeinflussen sich sogar wechselseitig, weil die Formulardaten in der Session zwischengespeichert wurden.
  • Möglicherweise ist aber auch die Motivation möglichst wenig Angriffsfläche für Hacker zu bieten. Um ein Angriff in Form von Session Fixation oder Session Hijacking vorzubeugen.

Im Folgenden wird beschrieben wie eine Lösung dieses Problems aussehen könnte. Der Ansatz orientiert sich am MVC-Konzep, wie er hier im Blog schon und in den Artikeln aufgezeigt wurde:

Der View

Ausgangspunkt wird wahrscheinlich immer ein View mit einem Formular sein. Das Formular könnte etwar so aussehen (Auszug aus der ecpp-Datei):

<form method="post" >
    <$$  SessionForm::Manager::getFormToken( request )  $>
    <input
        type="hidden"
        name="arg_delete_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_delete_account_button"
            value="pushed"
            type="submit">Löschen
    </button>
</form >

Die Besonderheit ist der Aufruf der statischen Funktion SessionForm::Manager::getFormToken(). Hier mit wird das Formular um ein automatisch generiertes Element erweitert. Request ist eine C++ Scoped-Variable, die zur Laufzeit in der ecpp-Umgebung, also dem View zur Verfügen steht. Diese Variable muss der Funktion getFormToken() mitgegeben werden. Der SessionForm::Manager wird den Token, den er in dem Webformular einbaut als session shared Variable ablegen, um später entscheiden zu können, ob die gesendeten Daten zu einem veralteten Formular gehören. Die Klasse SessionForm::Manager wurde von uns selbst erstellt. Der Aufbau der Klasse wird weiter unten erklärt.

Wird in einer Seite mit mehreren HTML-forms gearbeitet muss immer der selbe Token verwendet werden. Zum Beispiel so:

% std::string formToken = SessionForm::Manager::getFormToken( request );
<form method="post" action="EditAccount">
    <$$ formToken $>
    <i><b>"<$ s_accountList[i_account].getLogin_name() $>"</b></i>
    <input
        type="hidden"
        name="arg_edit_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_edit_account_button"
            value="pushed"
            type="submit">Bearbeiten
    </button>
</form >
<form method="post" >
    <$$ formToken $>
    <input
        type="hidden"
        name="arg_delete_account_id"
        value="<$ s_accountList[i_account].getID() $>" >
    <button name="arg_delete_account_button"
            value="pushed"
            type="submit">Löschen
    </button>
</form >

Da mit jedem Aufruf der Funktion getFormToken() ein neuer, allein gültiger, Token generiert wird, werden mit jedem Aufruf alle vorige Token annulliert (ungültig gemacht). Natürlich kann man die Generierung des Token auch in den Controller verlegen und über TNT_REQUEST_SHARED_VAR an den View übertragen. Gerade bei komplexeren Applikationen wird das helfen, die Aufgaben zwischen den Code-Teilen zu strukturieren.

Manager

Die Implementierung der Manager-Klasse, die wir oben in den View-Code schon verwendet haben, könnte so aussehen:


std::string Manager::getFormToken( tnt::HttpRequest& request )
{
    TNT_SESSION_SHARED_VAR( std::string, SESSIONFORM_AVAILABLE_TOKEN, () );
    std::string token = genRandomToken(16);
    token += ":" + OString::IntToStr( request.getSerial() );
    std::string formTokenInput = "";
    SESSIONFORM_AVAILABLE_TOKEN = token;
    return formTokenInput;
}

std::string Manager::genRandomToken ( const int len) {
    std::string randomString = "";
    static const char alphanum[] =
        "0123456789"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz";
    for ( int i = 0; i < len; ++i ) {
        int randNo = srand() % (sizeof(alphanum) - 1) ;
        randomString.push_back ( alphanum[randNo] );
    }
    log_debug( "randomString (Token): " << randomString );
    return randomString;
}

Interessant ist die Funktion request.getSerial(). Diese gibt nämlich eine fortlaufende Nummer zurück. Für jedem request wird eins hochgezählt. Damit wird verhindert, dass der große Zufall mal eintreten könnte, dass wir zweimal die selbe Zufallszahl haben.

Um die Qualität der Zufallszahlen zu erhöhen, sollte der folgende Funktionsaufruf in der main-Funktion (oder in einem anderen Teil des Codes der zu Initialisierung geeignet ist) stehen:

/* initialize random seed: */
srand (time(NULL));

Andernfalls wird man sich darüber wundern, das die „Zufallszahlen“ gar nicht so zufällig sind und damit die die Sicherheitsbemühungen wieder zunichte machen.

Routing

Was jetzt noch fehlt, damit die Komponente alle Formular-Daten überprüft, ist eine Route, die dafür sorgt, dass der Controller, der den Formular-Torken prüft, immer aufgerufen wird. Dazu muss die folgende Route gesetzt werden.

// controller rout for SessionForm token check.
app.mapUrl( "^/(.*)", "SessionForm::Controller" );

Sollen nur bestimmte Formulare-Seiten überwacht werden, muss die Route dementsprechend angepasst werden.

Weiterleitungen

Sollten Formulardaten von veralteten Seiten stammen, wird der Besucher auf die Fehler-Seite /SessionForm/NoAvailabeToken umgeleitet. Möchte man statt der Standard Seite eine eigene Seite gestalten, ist das Routing anzupassen:

// controller rout for SessionForm token check.
app.mapUrl( "^/SessionForm/NoAvailabeToken", "MyTokenErrorView" );

Controller

Der Controller der, die Verarbeitung prüft, könnte so aussehen:

unsigned SessionForm::Controller::operator() (
    tnt::HttpRequest& request,
    tnt::HttpReply& reply,
    tnt::QueryParams& qparam
) {
    TNT_SESSION_SHARED_VAR( std::string, SESSIONFORM_AVAILABLE_TOKEN, () );
    if ( qparam.has("SESSIONFORM_TOKEN") ) {

        std::string SESSIONFORM_TOKEN =
            qparam.arg<std::string>("SESSIONFORM_TOKEN");

        if ( SESSIONFORM_TOKEN != SESSIONFORM_AVAILABLE_TOKEN ) {
            return reply.redirect ( "/SessionForm/NoAvailabeTokenView" );
        }
    };
    return DECLINED;
}

Wer ein vollständiges Beispiel der Implementierung sehen will, kann sich den Code von meinem Projekt Peruschim auf GitHub anschauen.


Creative Commons Lizenzvertrag