borisbaer: In MVC-Umgebung bei JS-Validierung auf Datenbank zugreifen

Hallo! Ich möchte innerhalb meiner Javascript-Validierung eines Sign-up-Formulars „in Echtzeit“ prüfen, ob ein Benutzername bzw. eine E-Mail-Adresse bereits in der Datenbank existiert.

Prinzipiell geht das ja über den fetch-Befehl, doch das Problem ist, dass sich die Resultate meiner Datenbank-Zugriffe in den Controller-Klassen meines MVC-Frameworks befinden. Das JS-Skript kommt jedoch nicht an diese heran. Ich habe z.B. folgenden Code im SignUp-Controller, ...

$username = SignUpModel::read( [ 'username' => $_GET[ 'username' ] ] );
header( 'Content-Type: application/json' );
echo json_encode( [ 'username' => $username ] );

... der mir true oder false ausgibt, je nachdem ob der Benutzer bereits in der Datenbank existiert.

Vom Grundgedanken würde dann der fetch-Befehl ungefähr so aussehen:

return fetch( 'SignUpController.php?username='
	+ encodeURIComponent( inputValue ) )
	.then( function( response ) {
		return response.json();
	})
	.then( function( json ) {
		return json.username;
	});

Ich möchte eigentlich keine extra PHP-Dateien innerhalb des public-Ordners, in dem sich auch die JS-Dateien befinden. Kann ich das Ganze irgendwie umgehen?

Viele Grüße
Boris

  1. Hallo borisbaer,

    da ist Refaktorieren angesagt.

    Du brauchst einen UsernameVerifyController (beispielsweise), den du im Ajax Aufruf verwendest und der die Logik enthält.

    Dieser Controller könnte vom SignUpController als Untercontroller genutzt werden, oder du lagerst den gemeinsamen Code an einen passenden Ort aus.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      Du brauchst einen UsernameVerifyController (beispielsweise), den du im Ajax Aufruf verwendest und der die Logik enthält.

      also tatsächlich eine Art „Sub-Controller“ im public-Ordner? Ich hätte gedacht, dass man bei einer MVC-Ordnerstruktur niemals eine Datei mit einer PHP-Klasse dort lagern sollte. Ich habe in meinem Framework die Ordner app, core, public. Sinn der Sache bei einem MVC-Framework, dachte ich, sei, dass die Dateien im public-Ordner keine Ahnung von der Existenz irgendwelcher Dateien außerhalb dieses public-Ordners haben. Meine Frage ist also: Muss man, wenn man mit dem fetch-Befehl irgendetwas aus der Datenbank abrufen will, diese Ausnahme machen?

      Noch eine Frage: Du sprichst von Ajax-Aufruf. Warum? Ich dachte Ajax wäre nur für jQuery und fetch für Vanilla Javascript. Ist dem nicht so?

      Dieser Controller könnte vom SignUpController als Untercontroller genutzt werden, oder du lagerst den gemeinsamen Code an einen passenden Ort aus.

      Was könnte so ein passender Ort sein? Ich wüsste auch gar nicht, in welchen Unterordner ich dann diesen Untercontroller stecken sollte: includes, classes, php?

      Grüße
      Boris

      1. Hallo borisbaer,

        aaaalso.

        AJAX - ist eine Technik des Web 2.0, und die jQuery-Methode ist nach dieser Technik benannt. AJAX stand für Asynchronous JavaScript And XML. Man ist aber nicht auf XML als Content beschränkt und daher sollte X eher als "X-beliebiger Inhalt" gelesen werden.

        Wenn Du per JavaScript Daten nachlädst, sei es per $.ajax, XMLHttpRequest oder fetch, dann machst Du AJAX (Disclaimer: XMLHttpRequest hat auch einen "synchronen" Modus. Aber den sollte man meiden).

        Controller im public-Ordner - wo hast Du denn deine übrigen Controller? Anderswo? Über ein Routing identifiziert? Na dann - nutze das Routing auch für diesen AJAX-Request. Wie auch immer Du Deine URIs gerne gestalten möchtest - das hängt nur von deinem Router ab,

        • fetch("/api/checkUser/borisbaer")
        • fetch("/api/checkUser/borisb%C3%A4r")
        • fetch("/CheckUser?name=borisb%C3%A4r")

        Alles machbar. Ob Du API Funktionen über einen Ordner "api" bereitstellst, ist nicht zwingend. Ich mach das so in meinen Seiten.

        Wohin - ja was weiß ich denn? Das ist deine Codebasis. Wenn Du einen API Controller erstellst und der sich mit dem Signup-Controller den Code teilt, können beide Controller im Controller-Ordner sein und die Worker-Klasse mit dem eigentlichen Code in classes. Die Architektur deines Routers (über den wir mal diskutiert haben, den ich aber mit allen Pros und Cons nicht mehr im Kopf habe) mag da auch was vorgeben.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo Rolf,

          AJAX - ist eine Technik des Web 2.0, und die jQuery-Methode ist nach dieser Technik benannt. AJAX stand für Asynchronous JavaScript And XML. Man ist aber nicht auf XML als Content beschränkt und daher sollte X eher als "X-beliebiger Inhalt" gelesen werden.

          Wenn Du per JavaScript Daten nachlädst, sei es per $.ajax, XMLHttpRequest oder fetch, dann machst Du AJAX (Disclaimer: XMLHttpRequest hat auch einen "synchronen" Modus. Aber den sollte man meiden).

          danke für die Erklärung!

          Controller im public-Ordner - wo hast Du denn deine übrigen Controller? Anderswo? Über ein Routing identifiziert? Na dann - nutze das Routing auch für diesen AJAX-Request. Wie auch immer Du Deine URIs gerne gestalten möchtest - das hängt nur von deinem Router ab,

          • fetch("/api/checkUser/borisbaer")
          • fetch("/api/checkUser/borisb%C3%A4r")
          • fetch("/CheckUser?name=borisb%C3%A4r")

          Alles machbar. Ob Du API Funktionen über einen Ordner "api" bereitstellst, ist nicht zwingend. Ich mach das so in meinen Seiten.

          Wohin - ja was weiß ich denn? Das ist deine Codebasis. Wenn Du einen API Controller erstellst und der sich mit dem Signup-Controller den Code teilt, können beide Controller im Controller-Ordner sein und die Worker-Klasse mit dem eigentlichen Code in classes. Die Architektur deines Routers (über den wir mal diskutiert haben, den ich aber mit allen Pros und Cons nicht mehr im Kopf habe) mag da auch was vorgeben.

          Mir ist ein Denkfehler unterlaufen. Ich dachte, ich müsste im fetch-Befehl auf die entsprechende Controller-Datei verweisen, also VerifyUsername.php oder so mit absolutem Pfad. Aber der fetch-Befehl richtet sich ja auch danach, was im Router steht! Demnach kann ich einfach eine URL im fetch-Befehl schreiben und diese dann so im Router registrieren, dass sie zur richtigen Controller-Klasse führt. Dann muss ich nichts im public-Ordner speichern, sondern kann die Datei mit der VerifyUsername-Klasse getrost bei den anderen Controllern lagern, und zwar in app/controllers.

          🤦‍♂️

          Grüße
          Boris

  2. Hallo borisbaer,

    da fehlt Errorhandling. Und deine Minifunktionen kannst Du auch als Pfeilfunktionen schreiben - der IE, der sie nicht versteht, ist tot.

    response.ok ist false, wenn der Server einen HTTP-Status außerhalb des 2xx Bereichs liefert. Also 403, oder 500. Bei einem 302 hängt es von den fetch-Optionen ab, die Du gesetzt hast, ob fetch dem Redirect folgt oder einen Fehler meldet. Ich kann's grad nicht testen, ich weiß nicht ob bei einem Redirect der erste .then überhaupt aufgerufen wird (die Spec sagt: Mach einen Network Error draus) oder nur ok auf false steht. Aber deswegen hängt man einen catch hinten dran, um Fehler aus erstem .then und Fehler aus .fetch gemeinsam abzuhandeln.

    Wenn der erste then Handler eine Exception wirft oder ein rejected promise zurückgibt, geht die Promise-Aufrufkette beim nächsten .catch weiter. Gibt ein .catch-Handler ein "normales" Ergebnis zurück, entsteht wieder ein erfülltes Promise.

    return fetch( 'SignUpController.php?username=' + encodeURIComponent(inputValue))
       .then( response => {
          if (!response.ok)
             throw `${response.status} ${response.statusText}`; 
          return response.json();
       })
       .then( json => json.username )
       .catch( err => { 
          console.log("Validation of user name failed: " + err);
          return null;
       });
    

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      da fehlt Errorhandling. Und deine Minifunktionen kannst Du auch als Pfeilfunktionen schreiben - der IE, der sie nicht versteht, ist tot.

      stimmt, danke für den Hinweis!

      response.ok ist false, wenn der Server einen HTTP-Status außerhalb des 2xx Bereichs liefert. Also 403, oder 500. Bei einem 302 hängt es von den fetch-Optionen ab, die Du gesetzt hast, ob fetch dem Redirect folgt oder einen Fehler meldet. Ich kann's grad nicht testen, ich weiß nicht ob bei einem Redirect der erste .then überhaupt aufgerufen wird (die Spec sagt: Mach einen Network Error draus) oder nur ok auf false steht. Aber deswegen hängt man einen catch hinten dran, um Fehler aus erstem .then und Fehler aus .fetch gemeinsam abzuhandeln.

      Habe ich übernommen. Auch hier: danke für die Erklärung!

      Wenn der erste then Handler eine Exception wirft oder ein rejected promise zurückgibt, geht die Promise-Aufrufkette beim nächsten .catch weiter. Gibt ein .catch-Handler ein "normales" Ergebnis zurück, entsteht wieder ein erfülltes Promise.

      return fetch( 'SignUpController.php?username=' + encodeURIComponent(inputValue))
         .then( response => {
            if (!response.ok)
               throw `${response.status} ${response.statusText}`; 
            return response.json();
         })
         .then( json => json.username )
         .catch( err => { 
            console.log("Validation of user name failed: " + err);
            return null;
         });
      

      Also, der fetch-Aufruf funktioniert, ich erhalte in der Konsole folgende Meldung:

      Promise { <state>: "pending" }
      <state>: "fulfilled"
      <value>: true
      <prototype>: Promise.prototype { … }
      validation.js:179:12
      

      Aber wie komme ich an den Wert von <value> heran, um ihn dann zu verarbeiten?

      Übrigens hat mir Visual Code Studio vorgeschlagen, die function für den fetch-Aufruf zu einer sog. asnyc function zu machen. Dies würde dann so aussehen:

      async function verifyValue( input, value ) {
      	try {
      		const response = await fetch( `/verify?${input.name}=${encodeURIComponent( value )}` );
      		if ( !response.ok )
      			throw `${response.status} ${response.statusText}`;
      		const json = await response.json();
      		return json.username;
      	} catch ( error ) {
      		console.log( error );
      		return null;
      	}
      }
      

      Was bringt mir das? Gibt es etwas dabei zu beachten?

      Grüße
      Boris

      1. Hallo borisbaer,

        Aber wie komme ich an den Wert von <value> heran, um ihn dann zu verarbeiten?

        Promises müssen verkettet werden. Hier mal ohne viel Fehlerbehandlung:

        function checkUser(name) {
           return fetch("/foo/"+encodeUriComponent(name))  // liefert Promise 1
                  .then(response=>response.json())      // liefert Promise 2
                  .then(obj=>obj.username);             // liefert Promise 3
        }
        

        Was man wissen muss, ist, dass nicht nur fetch ein Promise zurückgibt. Es gibt noch viel mehr. fetch liefert das Promise 1. Die Ausführung dauert etwas, darum ist das pending. Auf diesem Promise 1 rufst Du .then auf. Dieses .then liefert Dir ein Promise 2, und auf diesem Promise 2 rufst Du den nächsten .then auf. Auch DER liefert wieder ein Promise und DAS gibst Du aus getData zurück. Lies Dir mal das hier durch.

        Sobald fetch die Responseheader gelesen hat, resolved das Promise 1. In Folge wird der Callback des ersten .then aufgerufen. Der ruft response.json() auf und gibt das Ergebnis zurück. ABER die json() Methode gibt selbst auch ein Promise zurück, das Promise 4. Denn sie muss erstmal I/O machen, fetch hat nur die Header gelesen und die Daten sind noch auf der Leitung.

        Wenn Du meinen Link nachgelesen hast, dann weißt Du, dass nun Promise 4 und 2 gekoppelt sind. Sobald json() fertig ist, resolved es Promise 4, damit Promise 2 und der Callback des zweiten .then läuft los.

        Der liefert einen String und resolved damit Promise 3.

        Das alles passiert aber erst, nachdem getData() schon läääängst zurückgekehrt ist. Der JavaScript-Task, der getData() aufgerufen hat, ist nicht mehr da. Er kann nicht mehr da sein, denn die Promise-Callbacks laufen in der Microtask-Queue (siehe Link!) und die startet erst, wenn der Haupttask durch ist.

        Du kommst an den Wert also nur heran, wenn Du Dich der Promise-Logik unterwirfst. Du bekommst von checkUser das Promise 3 zurück. Aber in dem Moment, wo es zurückkommt, hat es noch keinen Wert. Du musst Dich also selbst an das Versprechen ranhängen. Mit .then():

        checkUser("borisbaer")
        .then(function(username) {
           // Ist einer gekommen, dann Kollision melden
        });
        

        Diese ganze Callbäckerei ist natürlich lästig und deshalb hat man nach dem Konditor gerufen, damit er Zuckerguss draufmacht und async/await bekommen.

        await wartet darauf, dass ein Promise sich auf ein Ergebnis festlegt. Unter der Haube ist das aber immer noch nichts anderes als ein .then-Aufruf und alles, was hinter await in deiner Funktion steht, muss in den .then-Callback hinein. Die JavaScript-Engine baut also deinen Code für Dich um.

        Eine Konsequenz davon ist, dass deim Timing kaputt geht. Du schreibst:

        function checkUser(name) {
           let response = await fetch("/foo/"+name);
           if (response.ok) {
              let user = await response.json();
              return user;
           }
        }
        

        und siehst gar nicht mehr, dass die checkUser-Funktion nach dem ersten await erstmal fertig ist und der Rest in Microtasks abläuft. Um noch eine Ablaufreihenfolge hinzubekommen, muss checkUser ein Promise zurückgeben, und dafür dient async. Das async-Schlüsselwort sagt, dass diese Funktion ein Promise zurückgibt. Und das tut sich auch, selbst wenn Du nichts asynchrones darin machst. Wenn Du in einer async-Funktion return xy; programmierst, entspricht das einem resolve(xy); Aufruf. Endest Du ohne return, entspricht das einem resolve(undefined). Und wenn Du mit throw etwas wirfst, entspricht das dem Aufruf von reject.

        Heißt also: Du musst ENTWEDER das Ergebnis von checkUser awaiten, was Dir dann auferlegt, dass der Aufrufer von checkUser selbst wieder async sein muss. Das ist das Blöde an async, es ist ansteckend und bahnt sich seinen Weg bis ganz nach oben.

        In neueren JavaScript-Versionen ist ein "top level await" zulässig geworden. Das war es früher nicht, da MUSSTE man eine async-Funktion haben, um await machen zu können.

        Die Alternative zum await ist, einfach das Promise aus der async-Funktion als Promise zu nehmen und .then darauf aufzurufen.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo Rolf,

          vielen Dank für die ausführliche Erklärung!

          Lies Dir mal das hier durch.

          Habe ich getan, sehr erhellend, danke!

          Du kommst an den Wert also nur heran, wenn Du Dich der Promise-Logik unterwirfst. Du bekommst von checkUser das Promise 3 zurück. Aber in dem Moment, wo es zurückkommt, hat es noch keinen Wert. Du musst Dich also selbst an das Versprechen ranhängen. Mit .then():

          checkUser("borisbaer")
          .then(function(username) {
             // Ist einer gekommen, dann Kollision melden
          });
          

          Ich hab’s, denke ich, verstanden und hingekriegt! Hier wird das auch ganz gut erklärt.

          Danke auch für die Erklärungen zu async/await. Um es wirklich zu verstehen, muss ich aber mal bei Gelegenheit mehr zu dem Thema recherchieren.

          Übrigens habe ich die verify-Methode so umgeschrieben, dass sie sowohl für den Benutzernamen als auch für die E-Mail-Adresse funktioniert …

          public function verify(): never
          {
          	$attr = array_keys( $_GET )[1] ?? null;
          	if ( $attr === 'username' || $attr === 'email' ) {
          		$value = \App\Models\User\SignUp::read( [ $attr => $_GET[$attr] ] );
          		header( 'Content-Type: application/json' );
          		echo json_encode( [ 'value' => $value ] );
          		exit;
          	} else {
          		header( 'Location: user' );
          		exit;
          	}
          }
          

          Die Javascript-Funktion sieht dann entsprechend so aus:

          async function verify( input, value ) {
          	try {
          		const response = await fetch( `/verify?${input.name}=${encodeURIComponent( value )}` );
          		if ( !response.ok )
          			throw `${response.status} ${response.statusText}`;
          		const json = await response.json();
          		return json.value;
          	} catch ( error ) {
          		console.log( error );
          		return null;
          	}
          }
          

          Das mit header( 'Content-Type: application/json' ) habe ich von einem Tutorial. Es funktioniert aber auch ohne. Gebe ich das also nur für den Browser an?

          Grüße
          Boris

          1. Hallo borisbaer,

            public function verify(): never
            {
               $attr = array_keys( $_GET )[1] ?? null;
            	 if ( $attr === 'username' || $attr === 'email' ) {
               }
            }
            

            Ich nehme mal an, dass dies die Top-Level Funktion ist, die vom Router aufgerufen wird. Nur in diesem Fall würde ich dein Hantieren mit $_GET und dem Setzen der Header für korrekt halten.

            Nicht korrekt ist, dass sie im Fehlerfall einen Redirect liefert. Das ist sinnlos, du möchtest den fetch() umleiten?! Wenn Du etwas tun willst für den Fall, dass jemand die API-URL im Browser eingibt: tu's nicht. Wer APIs aufruft und HTML erwartet, hat Pech. Falsche Parametrierung kannst Du mit HTTP Statuscodes beantworten.

            Deine read-Funktion bekommt ein Array mit den Spalten, für die zu prüfen ist, ob der Wert schon in der DB existiert? Kann man so machen - aber was soll read tun? Kollisionstest für genau ein Attribut? In dem Fall würde die Methode vielleicht doch besser "exists($column, $value)" heißen und einfach ein bool zurückgeben?

            Und wer garantiert Dir, dass username oder email der zweite Parameter für deine Funktion sind? Davon solltest Du Dich entkoppeln und eine Prüffunktion schreiben, die schaut, ob einer von mehreren Parametern da ist. Ich habe sie erstmal ohne $this oder \App\Tools\Bla\Foo davor hingeschrieben - die kann man an geeignetem Ort unterbringen.

            use \App\Models\User\SignUp;
            
            ...
            function verify()
            {
               $attr = hasKey($_GET, ['username', 'email']);
               if (attr)
               {
                  // Potenzielles Sicherheitsloch!
                  $value = SignUp::read( [ $attr => $_GET[$attr] ] ); 
                  header( 'Content-Type: application/json' );
                  echo json_encode( [ 'value' => $value ] );      
               }
               else
               {
                  http_response_code(400);  // Bad Request, no content
               }
               exit;
            }
            
            ...
            
            // Mögliche Implementierungen von hasKey
            // Einzeiler mit 2 Array-Funktionen und ??
            function hasKey($array, $keys) {
               return array_intersect(array_keys($array), $keys)[0] ?? null;
            }
            // oder klassisch als Schleife:
            function hasKey($array, $keys)
            {
               foreach ($keys as $key)
               {
                  if (array_key_exists($array, $key) 
                  {
                     return $key;
                  }
               }
               return null;
            }
            

            Zu deiner read-Funktion und der Motivation, sie so zu gestalten, wie sie ist, kann ich nichts sagen. Soll sie einen User über einen beliebigen Schlüssel finden können, und gibt diesen User dann zurück? In dem Fall wäre der Rückgabewert viel zu umfangreich, du willst bestimmt nicht einen allgemeinen Userdaten-Reader für anonyme Benutzer bereitstellen. Deine verify-Schnittstelle sollte nicht mehr als einen Bool zurückgeben. "Geht" oder "Geht nicht". Nichts weiter.

            Rolf

            --
            sumpsi - posui - obstruxi
            1. Hallo Rolf,

              Ich nehme mal an, dass dies die Top-Level Funktion ist, die vom Router aufgerufen wird. Nur in diesem Fall würde ich dein Hantieren mit $_GET und dem Setzen der Header für korrekt halten.

              ich weiß nicht, ob das deine Frage beantwortet, aber im Router steht $router -> map( 'GET', 'verify', [ 'namespace' => 'user', 'controller' => 'sign-up', 'action' => 'verify' ] );.

              Nicht korrekt ist, dass sie im Fehlerfall einen Redirect liefert. Das ist sinnlos, du möchtest den fetch() umleiten?! Wenn Du etwas tun willst für den Fall, dass jemand die API-URL im Browser eingibt: tu's nicht. Wer APIs aufruft und HTML erwartet, hat Pech. Falsche Parametrierung kannst Du mit HTTP Statuscodes beantworten.

              Okay, das ist gut zu wissen, danke! Das habe ich geändert.

              Deine read-Funktion bekommt ein Array mit den Spalten, für die zu prüfen ist, ob der Wert schon in der DB existiert? Kann man so machen - aber was soll read tun? Kollisionstest für genau ein Attribut? In dem Fall würde die Methode vielleicht doch besser "exists($column, $value)" heißen und einfach ein bool zurückgeben?

              Momentan ist es noch so, dass ich eine universelle read-Funktion habe, bei der ich die Art und Weise des fetch-Befehls als Parameter festlegen kann. In diesem Fall wird tatsächlich nur true oder false ausgegeben, nicht die ganze Tabellenreihe als Objekt.

              Und wer garantiert Dir, dass username oder email der zweite Parameter für deine Funktion sind? Davon solltest Du Dich entkoppeln und eine Prüffunktion schreiben, die schaut, ob einer von mehreren Parametern da ist. Ich habe sie erstmal ohne $this oder \App\Tools\Bla\Foo davor hingeschrieben - die kann man an geeignetem Ort unterbringen.

              Hmm, wieso muss mir das jemand garantieren? Der fetch-Befehl des JS-Scripts hat doch klare Anweisungen. Aber wenn man die Methode sozusagen für einen Blindflug gewappnet sehen will, dann ja. Ich habe deinen Vorschlag mit hasKey aber übernommen! Vielen Dank! Warum hast du eigentlich „potenzielles Sicherheitsloch“ dort hingeschrieben?

              Zu deiner read-Funktion und der Motivation, sie so zu gestalten, wie sie ist, kann ich nichts sagen. Soll sie einen User über einen beliebigen Schlüssel finden können, und gibt diesen User dann zurück? In dem Fall wäre der Rückgabewert viel zu umfangreich, du willst bestimmt nicht einen allgemeinen Userdaten-Reader für anonyme Benutzer bereitstellen. Deine verify-Schnittstelle sollte nicht mehr als einen Bool zurückgeben. "Geht" oder "Geht nicht". Nichts weiter.

              Ja, wahrscheinlich werde ich die read-Funktion in mehrere mit einer spezifischen Aufgabe aufsplitten, anstatt eine zu haben, die sozusagen alles auslesen kann, was ich brauche.

              Viele Grüße
              Boris

              1. Hallo borisbaer,

                Hmm, wieso muss mir das jemand garantieren?

                Es gibt zwei Möglichkeiten, Argumente ihren Parametern zuzuordnen: Per Name (xy.php?foo=3&bar=2) oder per Position (myAction/3/2 oder auch xy.php?3&2). Du hast einen Bastard gezüchtet: Benannte positionierte Parameter. Dein 2 Jahre älteres Ich könnte davon verwirrt werden.

                Momentan ist es noch so, dass ich eine universelle read-Funktion habe, bei der ich die Art und Weise des fetch-Befehls als Parameter festlegen kann.

                D.h. Du hast viele Situationen, wo Du etwas lesen musst. Die kanalisierst Du alle in eine Funktion, und die nimmt die Argumente auseinander und findet heraus, welche Situation gemeint ist. In Sprachen wie Java (nicht Script) oder C# könnte man hier vermutlich Überladungen nutzen. In PHP gibt's das nicht. Ich glaube, ein solches Interface bildet man besser mit mehreren spezifischen Methoden ab.

                Warum hast du eigentlich „potenzielles Sicherheitsloch“ dort hingeschrieben?

                Schrub ich doch: Wenn das Ding ein User-Objekt liefet und Du es stumpf zurückgibst, liefert das viel zu viele Informationen nach draußen.

                Du kannst übrigens durchaus einen generischen Reader verwenden. Es ist okay, wenn der Controller das Ergebnis der read-Methode interpretiert und dementsprechend ein true oder false an den Client schickt.

                Rolf

                --
                sumpsi - posui - obstruxi
  3. Hi Boris Bärchen :),

    leider möchte ich heute nicht direkt auf deine Frage eingehen. Bin nämlich damit beschäftigt ein Brute-Force Script zu basteln um dein System zu hacken.

    www.borisbärchen.de?user=a
    www.borisbärchen.de?user=b
    www.borisbärchen.de?user=c

    naja irgendwann werde ich schon ein true (oder etwas ähnliches) zurück bekommen. Dann habe ich schonmal den Usernamen und habe das System bereits zu 50% gehackt. Jetzt nur noch das Passwort: www.borisbärchen.de?user=boris&password=a
    www.borisbärchen.de?user=boris&password=b

    irgendwann habe ich auch dein Passwort.

    Und was lernen wir daraus? Für Login Dinge darf es niemals niemals niemals (habe ich schon niemals geschrieben?) eine Schnittstelle geben!!!111 Vor allem nicht, wenn es User wie eine Freundin von mir gibt, die Passwörter vergibt die "123456xy" heißen.

    Deshalb überdenke nochmal eine Idee.

    Gruß
    1234T-Rex!

    1. Für Login Dinge darf es niemals niemals niemals (habe ich schon niemals geschrieben?) eine Schnittstelle geben!!!111 Vor allem nicht, wenn es User wie eine Freundin von mir gibt, die Passwörter vergibt die "123456xy" heißen.

      Brut-Force- oder meinetwegen Wörterbuch-Attacken verhindert man aber anders.

      • Verzögerung der Antwort, ggf. in Abhängigkeit von der Anzahl der Fehlversuche per IP und/oder Username ansteigend.
      • (Zeitweise) Sperrung von IPs bei solchen Attacken, welche regelmäßig DDoS-Attacken ähneln.
      • Zweifaktor-Authentifizierung
      • Vorschriften bezüglich Länge und Komplexitätsmaß des Benutzernamens und Passworts.

      Es ist faktisch egal, ob ein Angreifer sich notwendige Informationen aus echten Formularen zusammenpokt oder die Daten via API sendet, denn das HTTP[S]-Protokoll ist - streng genommen - schon eine API, welche durch das, was der TO und Du hier als „API“ auffasst, nur eine beschränkende Nutzung der umfassenderen API „HTTP“ ist…

        • Verzögerung der Antwort, ggf. in Abhängigkeit von der Anzahl der Fehlversuche per IP und/oder Username ansteigend.
        • (Zeitweise) Sperrung von IPs bei solchen Attacken, welche regelmäßig DDoS-Attacken ähneln.
        • Zweifaktor-Authentifizierung
        • Vorschriften bezüglich Länge und Komplexitätsmaß des Benutzernamens und Passworts.

        Letzteres habe ich bereits implementiert. Hier könnte man natürlich noch darüber sprechen, welche Vorgaben gemacht werden sollten. Klein- und Großbuchstaben, Ziffern, Sonderzeichen, Mindestlänge, keine drei gleichen Zeichen hintereinander oder so was. Am Ende hängt es aber trotzdem vom Benutzer selber, dass er nicht P4$$w0rd als Passwort verwendet.

        Die ersten beiden Absicherung müssten wohl auch relativ einfach umzusetzen sein. Ob eine Zwei-Faktor-Authentifizierung bei meiner Website nötig ist, weiß ich nicht. So vertraulich sind die Daten eigentlich auch wieder nicht.

        Grüße
        Boris

        1. Moin in die Runde

          vor längerem war schon mal Brute Force in einem anderen Beitrag thematisiert worden.

          Auch da hab ich mich gefragt, weshalb eigentlich nicht auch das Stichwort mod_qos gefallen ist. Hier kann für bestimmte HTTP-Statuscodes und Anzahl von Zugriffen in einer festzulegenden Zeiteinheit vom Server-Betreiber eine IP-Sperrung durch das Apache-Modul festgelegt werden.

          Was meint ihr dazu?

          Gruß Claus

          1. Moin in die Runde

            vor längerem war schon mal Brute Force in einem anderen Beitrag thematisiert worden.

            Auch da hab ich mich gefragt, weshalb eigentlich nicht auch das Stichwort mod_qos gefallen ist. Hier kann für bestimmte HTTP-Statuscodes und Anzahl von Zugriffen in einer festzulegenden Zeiteinheit vom Server-Betreiber eine IP-Sperrung durch das Apache-Modul festgelegt werden.

            Was meint ihr dazu?

            mod_evasive gehört zum Standardumfang einer Apache-Installation (muss aber aktiviert werden) - den Rest würde ich der serverseitigen Programmierung überantworten, weil ja nicht so zwingend klar ist, dass das Projekt a) auf einem Apache und b) auf einem Server installiert und betrieben wird, auf dem man einfach mal Module installieren kann.

            Zudem sah es auf den ersten Blick so aus, als würde mod_qos vor allem auf http-auth reagieren. Und das, naja, sollte kann man nicht verwenden, wenn es nicht nur darum geht, 1-3 internen Nutzern Zugang zu verschaffen.

    2. Hallo T-Rex,

      diesen Hacking-Versuch kannst Du genausogut mit dem POST des Signup-Forms fahren. Denn das muss ja auch maulen, wenn man einen existierenden User oder eine existierende Mailadresse registrieren will.

      Deswegen muss man beides gegen Böse Spionagebots absichern.

      Verteidigungslinie 1
      Beim Programmieren der Seite darauf achten, den Ajax-Service nicht zu oft aufzurufen. Wenn nach jedem Tastendruck geprüft wird, ob der User schon existiert, ist das zu viel. Höchstens nach dem Verlassen des Username-Eingabefeldes (blur-Event). Zu häufige Aufrufe machen der Linie 2 und 4 das Leben schwerer.
      Verteidigungslinie 2
      Life-Analyse der eingehenden Requests auf Hochfrequenz-Abrufe. Ich bin kein Experte an dieser Stelle - aber es gibt bspw. Tools, die die Apache-Logs analysieren und bei bestimmten Mustern die IP sperren, die unangenehm auffällt. Das hilft aber nur gegen einzelne Angreifer. Eine verteilte Attacke aus einem Botnetz ist schwerer zu erkennen. Dies in PHP umzusetzen ist auch möglich, aber eigentlich gehören solche Erkennungen auf die Webserver-Ebene.
      Verteidigungslinie 3:
      Cross Site Request Forgery Abwehrmaßnahmen. Mit getrennten XSRF-Tokens für POST und AJAX-Call.
      Verteidigungslinie 4:
      Mitzählen im AJAX-Service. Dein Bot könnte die Browsermütze aufsetzen, per GET die Signup-Seite abrufen und sich so das XSRF-Token sowie die Session-ID beschaffen. Damit kann er dann fleißig User-IDs überprüfen. Um das zu verhindern, darf das XSRF-Token nur für eine bestimmte, kleine Anzahl von Aufrufen gültig sein. Ist diese überschritten, meldet der AJAX-Call einfach immer nur "Kenn ich nicht".
      Natürlich kann man das durch Erstellen einer neuen Session aushebeln. Aber das ist dann (a) mehr Arbeit für den Bot und (b) fällt er dann noch eher der Verteidigungslinie 2 auf.
      Ein regulärer User kann natürlich diese Schwelle ebenfalls überschreiten. Aber das macht nichts. Der offizielle POST des Signup-Formulars muss die gleiche Prüfung ohnehin auch noch mal machen, und weist die User-ID oder Mailadresse zurück.
      Verteidigungslinie 5:
      ▪️ Bei einem Fehl-Login nicht mitteilen, ob User oder Passwort falsch war
      ▪️ Zu viele Fehl-Logins für einen bekannten User: den User sperren oder zum Lösen eines Captchas verdammen
      ▪️ Zu viele Login-Versuche einer IP mit unbekannten User-IDs: IP für 24h bannen

      Andererseits - Komfort und Security passen nicht wirklich zusammen. Die Life-Überprüfung ist Komfort und vielleicht nice-to-have, aber nicht nötig. Eine sofortige Bestätigung des Signup ist ebenfalls Komfort und nicht unbedingt nötig. Beides bietet Schnüfflern Angriffspunkte.

      Eine Absicherung gegen Schnüffler erreicht man auch, indem man überhaupt nicht live antwortet, sondern grundsätzlich und immer mitteilt, dass man unter der angegebenen Mailadresse eine Mail mit einer Beschreibung der weiteren Schritte vorfinden würde. Damit ist das Raten von Mailadressen schonmal beseitigt. Das Raten von Usernamen via Signup bedeutet nun, dass der Bot nicht nur HTTP sprechen, sondern auch noch die Antwortmails analysieren muss. Kann man machen, sicher, ist aber eine Schwelle mehr. Möchte man sich auch dagegen absichern, könnte man wieder zu Frequenzanalysen greifen und die Anzahl von Signup-Versuchen von einer IP oder die Anzahl von Registrierungsmails an eine Adresse tracken. Ein Angreifer müsste das mit einem Botnetz sowie einer größeren Menge von Postfächern, vorzugsweise bei unterschiedlichen Anbietern, kontern. Und damit kommt man in den Bereich, wo eine Attacke auf die Seite einfach nicht mehr lohnt. Seiten, bei denen es sich dennoch lohnt, sollten ohnehin keinen automatischen Signup-Prozess haben.

      Die Anzahl von Registrierungsmails muss man eh tracken, zumindest pro Mailserver, andernfalls kann man durch einen solchen Schnüffelangriff auf die Spam-Liste befördert werden.

      Rolf

      --
      sumpsi - posui - obstruxi
    3. Hi Brute-Rex,

      leider möchte ich heute nicht direkt auf deine Frage eingehen. Bin nämlich damit beschäftigt ein Brute-Force Script zu basteln um dein System zu hacken.

      www.borisbärchen.de?user=a
      www.borisbärchen.de?user=b
      www.borisbärchen.de?user=c

      ich habe noch ein Konto bei NexusMods, an das ich nicht mehr herankomme. Ich kenne nur noch den Benutzernamen. Vielleicht kannst du mir dafür ein Script schreiben?

      naja irgendwann werde ich schon ein true (oder etwas ähnliches) zurück bekommen. Dann habe ich schonmal den Usernamen und habe das System bereits zu 50% gehackt. Jetzt nur noch das Passwort: www.borisbärchen.de?user=boris&password=a
      www.borisbärchen.de?user=boris&password=b

      Nun, bei password würde aber nichts herauskommen, da die Methode nur auf die Attribute username und email registriert ist. Kann man das aushebeln?

      irgendwann habe ich auch dein Passwort.

      Und was lernen wir daraus? Für Login Dinge darf es niemals niemals niemals (habe ich schon niemals geschrieben?) eine Schnittstelle geben!!!111 Vor allem nicht, wenn es User wie eine Freundin von mir gibt, die Passwörter vergibt die "123456xy" heißen.

      Sieht man allerdings sehr häufig. Also Live-Überprüfung des Benutzernamens bzw. der E-Mail-Adresse. Wenn das so leicht zu hacken ist, weshalb machen dies so viele Seite? Unwissenheit?

      Deshalb überdenke nochmal eine Idee.

      Ich denke nach!

      Grüße
      12drölf🧸