claus ginsel: wiederholtes Absenden Formular

Hallo

es geht um ein Formular, in dem ich einen Dateinamen eingebe und die Datei bei Vorliegen aller Voraussetzungen runter geladen wird. Nach dem Download ist das Textfeld mit dem Dateinamen weiterhin gefüllt. Das ist insoweit in Ordnung, ich kann den Download auch wiederholen.

Nun habe ich die Empfehlungen des BSI zur Websicherheit berücksichtigen wollen und auch einen CSRF-Schutz eingebaut. Nunmehr wird der 1. Download getätigt, aber weitere scheitern am CSRF-Schutz.

<?php
//...

session_start();
//...
if(isset($_POST['form_Download'])) 
{
	if (!isset($_SESSION['CSRF_DL'])) exit(...); 
	if(preg_match('/^[a-z0-9]{40}$/', $_SESSION['CSRF_DL'])==false) exit(...);
//diese Zeile ist das Problem bei wiederholten DL, redirect wird ausgeführt!
	if(!isset($_POST[$_SESSION['CSRF_DL']])) exit(...);
//...
?>
<HTML lang='de'>
...
<form action="index.php" method="POST">
	<input 
     type="hidden" 
     name="
       <?php 
         $_SESSION['CSRF_DL'] = sha1(uniqid('', true)); 
         echo $_SESSION['CSRF_DL'];  
       ?>" 
     value="1" 
  />
	<input type="text" name="input_datei" required>
	<input name="form_Download" type="submit" value="Bestätigen">
</form>
...
</HTML>

Könnt Ihr mir einen Tipp geben, wie ich mehrfaches Absenden und CSRF unter einen Hut kriege?

Bei der Gelegenheit, ich muss zugeben, ich habe den CSRF-Schutz mechanisch zugefügt. Die Beispiele für Gefährdungen sind ja soweit auch einleuchtend, aber ich kann jetzt nicht beurteilen, ob mein spezieller Anwendungsfall durch CSRF gefährdet ist. Was meint Ihr dazu?

Gruß Claus

akzeptierte Antworten

  1. Tach!

    Nun habe ich die Empfehlungen des BSI zur Websicherheit berücksichtigen wollen und auch einen CSRF-Schutz eingebaut. Nunmehr wird der 1. Download getätigt, aber weitere scheitern am CSRF-Schutz.

    Weil der Token am Server nicht mehr derselbe ist. Du müsstest die Formularseite erneut mit gültigem Token aufrufen.

    Könnt Ihr mir einen Tipp geben, wie ich mehrfaches Absenden und CSRF unter einen Hut kriege?

    Bei der Gelegenheit, ich muss zugeben, ich habe den CSRF-Schutz mechanisch zugefügt. Die Beispiele für Gefährdungen sind ja soweit auch einleuchtend, aber ich kann jetzt nicht beurteilen, ob mein spezieller Anwendungsfall durch CSRF gefährdet ist. Was meint Ihr dazu?

    Das musst du selbst einschätzen. CSRF ist ein Problem, wenn der Request Daten ändert und der Nutzer dafür angemeldet sein muss. Wenn der Download-Request keine Daten ändert, muss er auch nicht gegen CSRF gesichert sein.

    dedlfix.

    1. Aha, besten Dank

      Also die Seite dient ausschließlich dem Download, also könnte ich auf CSRF-Schutz verzichten ...

      Eines verstehe ich noch nicht

      Weil der Token am Server nicht mehr derselbe ist.

      Aber den, also $_SESSION['CSRF_DL'], setze ich doch nur inline, wenn die Seite index.php auch ausgeliefert wird, das wird sie doch nicht beim Datei-Download, denn dann müsste das Formularfeld ja leer sein 🤔

      Du müsstest die Formularseite erneut mit gültigem Token aufrufen.

      Genau das krieg ich nicht automatisch hin. Setze ich nach dem Download-Aufruf einen redirect zB auf die Seite index.php, wird der Download auch nicht vollzogen.

      1. Tach!

        Weil der Token am Server nicht mehr derselbe ist.

        Aber den, also $_SESSION['CSRF_DL'], setze ich doch nur inline, wenn die Seite index.php auch ausgeliefert wird, das wird sie doch nicht beim Datei-Download, denn dann müsste das Formularfeld ja leer sein 🤔

        Ich sehe in deinem Code nicht, in welchen Dateien er steht, und damit auch nicht, wie das konkret bei dir abläuft. Wie die Datei ausgeliefert wird, ist gar nicht zu sehen. Wenn der Code nicht mehr gültig ist, muss deine Verarbeitung dort vorbeigekomen sein, wo ein neuer generiert wird. Soweit so logisch.

        dedlfix.

        1. nur hier wird der token gesetzt:

          <form action="index.php" method="POST">
          	<input 
               type="hidden" 
               name="
                 <?php 
                   $_SESSION['CSRF_DL'] = sha1(uniqid('', true)); 
                   echo $_SESSION['CSRF_DL'];  
                 ?>" 
               value="1" 
            />
          

          also nur dann, wenn die Seite index.php ausgeliefert wird, sonst nicht

          1. Lieber claus,

            	<input 
                 type="hidden" 
                 name="
                   <?php 
                     $_SESSION['CSRF_DL'] = sha1(uniqid('', true)); 
                     echo $_SESSION['CSRF_DL'];  
                   ?>" 
                 value="1" 
              />
            

            da fehlt die kontextgerechte Behandlung von Daten. Auch wenn der Wert in $_SESSION['CSRF_DL'] keine HTML-relevanten Zeichen (< " >) beinhalten mag, so gehört es zu sauberem Programmieren dazu, dass man Daten kontextgerecht behandelt:

            <input name="<?php
              echo htmlspecialchars($_SESSION['CSRF_DL']);
            ?>" value="1" />
            

            Liebe Grüße

            Felix Riesterer

          2. Tach!

            nur hier wird der token gesetzt:

            <form action="index.php" method="POST">
            

            also nur dann, wenn die Seite index.php ausgeliefert wird, sonst nicht

            Wenn du das Formular absendest, rufst du ebenfalls die index.php auf. Wird dabei der HTML-Teil ausgeklammert oder auch abgearbeitet?

            dedlfix.

            1. moin noch mal zum späten Abend

              Wenn du das Formular absendest, rufst du ebenfalls die index.php auf.

              ja, dann kommt php, prüft die Session, prüft auf POST-Variablen vom Formular, in dem Fall existieren diese, sendet die Datei, danach ist Schluss!!

              Wird dabei der HTML-Teil ausgeklammert oder auch abgearbeitet?

              Meiner Meinung nach wird der nicht mehr abgearbeitet. Denn die Formularfelder bleiben gefüllt. Ich hatte mal nach dem Senden der Datei zum Testen echos eingebaut, die wurden nicht mehr ausgegeben. Allerdings ein nachfolgender Redirect wurde ausgeführt, jedoch unterbleibt dann das Senden der Datei. Und da der HTML-Teil eben nicht mehr ausgegeben wird, wird auch der Token nicht verändert. So ein HTTP-Request kann wohl nicht zwei Sachen gleichzeitig senden, deshalb entweder das Action-Script mit leeren Formularfeldern oder die Datei.

              Gruß Claus

              1. Hallo Claus,

                Wird dabei der HTML-Teil ausgeklammert oder auch abgearbeitet?

                Meiner Meinung nach wird der nicht mehr abgearbeitet. Denn die Formularfelder bleiben gefüllt.

                wenn im PHP-Teil ausdrücklich eine Anweisung steht, die das Script beendet (z.B. ein exit), dann hast du recht. Wenn nicht, wird es interessant: Wie übermittelst du die Datei zum Download an den Client?
                Direkt durch das PHP-Script mit readfile() und vorher dem korrekten Content-Type- und dem zur Dateigröße passenden Content-Length-Header? Dann würde alles, was nach dem PHP-Block (also nach dem ?>) noch folgt, ebenfalls übertragen, aber vom Client ignoriert, weil es über die angekündigte Content-Length hinausgeht.
                Oder durch ein Redirect (301 oder 302) auf die angeforderte Datei? Dann hättest du zwar recht, aber der eigentliche Download erfolgt dann, indem der Client dem Redirect folgt und einen zweiten HTTP-Request absetzt, der die Ressource direkt abruft.

                Ich hatte mal nach dem Senden der Datei zum Testen echos eingebaut, die wurden nicht mehr ausgegeben. Allerdings ein nachfolgender Redirect wurde ausgeführt, jedoch unterbleibt dann das Senden der Datei.

                Das ist allerdings sehr mysteriös. Nicht das übliche Verhalten. Es deutet darauf hin, dass du sehr ungewöhnliche Methoden verwendest, die ich mir im Moment nicht vorstellen kann - vielleicht Output Buffering?

                Und ja, wenn der Client sich entschließt, dem Redirect zu folgen, ignoriert er den eigentlichen Inhalt (den Response Body) der HTTP-Antwort. Das ist korrekt so.

                So ein HTTP-Request kann wohl nicht zwei Sachen gleichzeitig senden

                Richtig, so funktioniert HTTP: Eine Anfrage, eine Antwort. Je nachdem, wie die Antwort ausfällt, kann sie den Client dazu bringen, eine weitere Anfrage zu senden - etwa beim Redirect.

                Live long and pros healthy,
                 Martin

                --
                Klein φ macht auch Mist.
                1. Moin Martin,

                  wie Du das so schreibst, wird das Verhalten klar

                  <?php
                  	function makeDownload($Dateiname_Server, $Datei_Pfad, $Datei_MIME, $Dateiname_Client) 
                  	{
                  		header('X-Content-Type-Options: nosniff');			
                  		header("Content-Type: ".$Datei_MIME);
                  		header("Content-Disposition: attachment; filename=".$Dateiname_Client);
                  		header("Content-Length: ".filesize($Datei_Pfad.$Dateiname_Server));
                  		readfile($Datei_Pfad.$Dateiname_Server);
                  	} 
                  ?>
                  

                  readfile ist die letzte Anweisung in php, es gibt nichts mehr wie exit, also wird der HTML-Teil auch übertragen, nur vom Browser wegen Content-Length nicht berücksichtigt.

                  Angenommen, ich würde Content-Length um den Betrag der Index.php erhöhen, dann würde diese mit ausgegeben. Allerdings durch Content-Disposition vom Browser nicht als gerendertes HTML, sondern die Quelldaten von Index.php an die Download-Datei rangehängt. Das wäre im Ergebnis so, als würde ich vor Absenden der Header eine Ausgabe tätigen, worauf ich früher auch mal reingefallen bin.

                  Falls aus

                  Allerdings ein nachfolgender Redirect wurde ausgeführt, jedoch ...

                  ein Missverständnis erwachsen sein sollte: Ich habe den Begriff Redirect für jegliche Weiterleitung über header(Location: ...) benutzt. Möglicherweise versteht Ihr darunter lediglich die Statuscode 301 und 302?

                  Besten Dank für die erhellenden Infos

                  Gruß Claus

                  1. Hallo,

                    <?php
                    	function makeDownload($Dateiname_Server, $Datei_Pfad, $Datei_MIME, $Dateiname_Client) 
                    	{
                    		header('X-Content-Type-Options: nosniff');			
                    		header("Content-Type: ".$Datei_MIME);
                    		header("Content-Disposition: attachment; filename=".$Dateiname_Client);
                    		header("Content-Length: ".filesize($Datei_Pfad.$Dateiname_Server));
                    		readfile($Datei_Pfad.$Dateiname_Server);
                    	} 
                    ?>
                    

                    readfile ist die letzte Anweisung in php, es gibt nichts mehr wie exit, also wird der HTML-Teil auch übertragen, nur vom Browser wegen Content-Length nicht berücksichtigt.

                    genau das wollte ich damit sagen.

                    Angenommen, ich würde Content-Length um den Betrag der Index.php erhöhen, dann würde diese mit ausgegeben. Allerdings durch Content-Disposition vom Browser nicht als gerendertes HTML, sondern die Quelldaten von Index.php an die Download-Datei rangehängt.

                    Korrekt.

                    Das wäre im Ergebnis so, als würde ich vor Absenden der Header eine Ausgabe tätigen, worauf ich früher auch mal reingefallen bin.

                    Nein, mit der ersten Ausgabe (purer HTML-Code odere PHP-Ausgaben, z.B. print oder echo) werden alle bis dahin vorgemerkten HTTP-Header gesendet. Eine später folgende header-Anweisung kann dann nicht mehr berücksichtigt werden[1]. Versucht man es trotzdem, bekommt man von PHP die Meldung Cannot modify header information - headers already sent.

                    Falls aus

                    Allerdings ein nachfolgender Redirect wurde ausgeführt, jedoch ... ein Missverständnis erwachsen sein sollte: Ich habe den Begriff Redirect für jegliche Weiterleitung über header(Location: ...) benutzt. Möglicherweise versteht Ihr darunter lediglich die Statuscode 301 und 302?

                    Ja. Ein HTTP-Redirect, wie du ihn beschreibst, setzt automatisch auch einen der beiden Statuscodes 301 oder 302. Einer von beiden bedeutet eine kurzzeitige Umleitung, z.B. bei Wartungsarbeiten, der andere eine dauerhafte (nur die Zuordnung kann ich mir nie merken).

                    Live long and pros healthy,
                     Martin

                    --
                    Klein φ macht auch Mist.

                    1. Es sei denn man aktiviert Output Buffering. Dabei wird die Ausgabe bis zum Schluss des Scripts in einem Puffer gesammelt und erst dann ausgegeben. ↩︎

                  2. Der Ausgangspunkt meiner Anfrage war ja ein anderer, der sich mit Martins Antwort aber nun auch auflöst: Wenn also der HTML-Teil entgegen meiner Annahme doch ausgeführt wird, dann wird ja auch $_SESSION['CSRF_DL'] und damit der CSRF-Token verändert, aber das Formular im Browser enthält weiterhin das Feld mit dem alten Wert.

                    Damit hab ich eigentlich alles. Ich werde, wie dedlfix anregte, beim reinen Download auf eine CSRF-Prüfung verzichten. Und ich werde Felix seinen Tipp, statt mit Eingaben in ein Textfeld mit Links auf die Dateien zu arbeiten, umsetzen. Spart mir am Ende Arbeit 😀

                    Ich danke allen für die Hinweise

                    bis zu nächsten Mal

                    Gruß Claus

  2. Nun habe ich die Empfehlungen des BSI zur Websicherheit berücksichtigen wollen

    Das ist schon mal sehr löblich.

    Bei der Gelegenheit, ich muss zugeben, ich habe den CSRF-Schutz mechanisch zugefügt. Die Beispiele für Gefährdungen sind ja soweit auch einleuchtend, aber ich kann jetzt nicht beurteilen, ob mein spezieller Anwendungsfall durch CSRF gefährdet ist. Was meint Ihr dazu?

    Das ist allerdings eine gefährliche Vorgehensweise. Software-Sicherheit ist ein schwieriges Thema und Halbwissen auf dem Gebiet kann im besten Fall zu einem trügerischen Sicherheitsgefühl beitragen und im schlimmsten Fall mehr Türen für Angreifen öffnen als schließen . Bevor du anfängst etwas zu programmieren, solltest du die Theorie verstanden haben, und auf deinen Anwendungsfall übertragen können. Du solltest auch in der Lage die Vor- und Nachteile gängiger Implementierungen abzuwägen, bevor du selber etwas strickst. Eine gute erste Anlaufstelle zu CSRF ist das CSRF Prevention Cheat Sheet der OWASP Foundation.

    1. Software-Sicherheit ist ein schwieriges Thema

      da muss ich Dir vollumfänglich zustimmen 👍

      OWASP steht schon in meinen Favoriten, trotzdem danke für deinen Hinweis

  3. $_SESSION['CSRF_DL'] = sha1(uniqid('', true)); 
    

    uniqid() ist kein kryptographisch sicherer Zufallsgenerator, den du für solche Zwecke verwenden solltest.

    Primitive kryptographische Funktionen sollte man im Allgemeinen besser nicht selber programmieren. Vielleicht guckst du dir mal die Anti-CSRF Library an, die wird von Sicherheits-Experten entwickelt und getestet.

    1. nur mal zu meinem Verständnis:

      für CSRF bedarf es einiger Voraussetzungen

      • Session-basierte Anwendung
      • der betreffende User muss angemeldet sein
      • der Angreifer muss den Aufbau der Seite kennen
      • der Angreifer muss dem User die gefälschte Seite unterschieben

      Wie oft kann der Angreifer den User dazu veranlassen, die präparierte Seite aufzurufen, so dass der Angreifer verschiedene Werte für den Token ausprobieren kann? Ich denke, nur wenige Male.

      Weshalb muss dann der Token kryptografisch sicher sein, hab ich einen Denkfehler?

      1. nur mal zu meinem Verständnis:

        für CSRF bedarf es einiger Voraussetzungen

        • Session-basierte Anwendung
        • der betreffende User muss angemeldet sein
        • der Angreifer muss den Aufbau der Seite kennen
        • der Angreifer muss dem User die gefälschte Seite unterschieben

        Das stimmt so im Groben und Ganzen.

        Wie oft kann der Angreifer den User dazu veranlassen, die präparierte Seite aufzurufen, so dass der Angreifer verschiedene Werte für den Token ausprobieren kann? Ich denke, nur wenige Male.

        Hat der Angreifer sein Opfer einmal auf seine Seite gelockt, kann er beliebig viele Angriffsversuche unternehmen, sofern die Zielseite keine weiteren Gegenmaßnahmen ergreift.

        Weshalb muss dann der Token kryptografisch sicher sein, hab ich einen Denkfehler?

        In der Software-Sicherheit gilt eine umgekehrte Beweislast: Du hast du zeigen, dass ein Verfahren hinreichend ist, um gegen einen bestimmten Angriffsvektor abzusichern. Dazu ist es wichtig die Annahmen, unter welchen das Verfahren effektiv schützt, klar zu benennen. Im Falle von Verteidigungen gegen CSRF ist eine übliche Annahme, dass ein CSRF-Token mit einem kryptografisch sicheren Zufallsgenerator generiert wird. Ohne diese Annahme gilt schlicht die Argumentationslinie nicht, mit der das Verfahren als hinreichend sicher eingestuft werden kann. Du kannst natürlich versuchen zu zeigen, dass ein Verfahren auch mit der schwächeren Vorbedingung eines nicht kryptografisch sicheren Zufallsgenerators hinreichend sicher ist. Aber das wird schwierig. Kryptografisch unsicherer Zufall ist ein kontraintuitives Gebilde. Aus kryptographischer Sicht gibt es nur ein paar wenige, verhältnismäßig einfach zu bestimmendende Parameter, die den "Zufall" vollständig vorhersehbar machen. Um die Probleme von unsicherem Zufall sichtbar zu machen, hilft es sich vorzustellen der Zufallswert sei immer konstant und bekannt. Das offenbart sehr deutlich wo die Schwachstellen liegen. Das mag nach einer starken Vereinfachung klingen, aber es hat schon so viele erfolgreiche Angriffe auf unsichere Zufallsgeneratoren gegeben, dass es sich dabei inzwischen um eine gängige Arbeitshypothese handelt. Und es bleibt natürlich die Frage, wieso das Risiko eingehen, wenn ein kryptografisch sicheres Zufallsverfahren verfügbar ist?

  4. Lieber claus,

    es geht um ein Formular, in dem ich einen Dateinamen eingebe und die Datei bei Vorliegen aller Voraussetzungen runter geladen wird.

    warum keinen Download-Link, sondern dieses Formular? Wenn Du die Session-ID bei jedem Request änderst, ist ein solcher Download-Link auch gegen CSRF geschützt. Wer Deine Session kapern will, muss vor Dir eine weitere Seite aufrufen, damit er die neueste Session-ID erhält und Du Dich neu anmelden musst.

    Nach dem Download ist das Textfeld mit dem Dateinamen weiterhin gefüllt.

    Wozu Tippfehler zumuten? Verwendest Du das Formular rein wegen Gegenmaßnahmen zu CSRF?

    Liebe Grüße

    Felix Riesterer

    1. Hi Felix

      Verwendest Du das Formular rein wegen Gegenmaßnahmen zu CSRF?

      Nein, nur für diesen Download. CSRF hab ich nachträglich ergänzt.

      Die Idee mit dem Download-Link gefällt mir auch in anderer Hinsicht. ZZ krieg ich für jede Datei, die mir zugestellt werden soll, eine Mail mit dem Dateinamen, den ich dann ins Formularfeld rein kopiere (deshalb mehrfaches Absenden des Formulars). Warum nicht gleich einen Link auf die Dateien in die Seite einfügen 👍

      Was den Kontextwechsel angeht, da hab ich an der Stelle lange drüber nachgedacht, ob das Ergebnis einer php-Funktion auch als Benutzereingabe zu betrachten ist. Ich denke nein. Aber ich lass mich gern eines besseren belehren.

      Danke Dir und Gruß

      1. Lieber claus,

        Was den Kontextwechsel angeht, da hab ich an der Stelle lange drüber nachgedacht, ob das Ergebnis einer php-Funktion auch als Benutzereingabe zu betrachten ist. Ich denke nein.

        da ist er schon der Fehler! Du sollst nicht nach $ist_benutzereingabe unterscheiden! Das ist Blödsinn, weil es den Blick auf das Wesentliche versperrt: Den Kontext. Um allein den geht es. Das Kapitel zum Kontextwechsel solltest Du Dir unbedingt zeitnah verinnerlichen!

        Liebe Grüße

        Felix Riesterer

    2. Wenn Du die Session-ID bei jedem Request änderst, ist ein solcher Download-Link auch gegen CSRF geschützt. Wer Deine Session kapern will, muss vor Dir eine weitere Seite aufrufen

      Dieses Verfahren wird häufig in Verbindung mit Session-Fixation-Angriffen genannt, aber es hat leider bekannte Schwachstellen.

      Zum einen entsteht dadurch eine Race-Condition: Wem gelingt es zuerst einen Request abzusetzen, um einen neue Session-ID zu generieren, dem Angreifer oder dem Nutzer? Die Startbedingungen stehen besser für den Angreifer, da er die Abfrage automatisiert stellen kann. So kann der Angreifende das Opfer von der Nutzung der Seite aussperren, indem er schneller als das Opfer neue Session-IDs generiert.

      Zum anderen wird der Server ggf. anfällig für DOS-Angriffe: Wenn der Server einen Hardware-getriebenen Zufallsgenerator benutzt, muss der Server erst genügend Umgebungs-Entropie sammeln, um Session-IDs zu generieren. Ein Angreifer kann das ausnutzen: er kann in sehr hoher Frequenz Anfragen an den Server schicken, die ihn zwingen neue Session-IDs zu erzeugen. Bei jedem Request wird der Entropie-Buffer wieder geleert und der Server muss warten, bis er wieder genügend befüllt ist, um die nächste ID zu generieren. Macht er das in einer ausreichend hohen Frequenz, dann hat der Server keine Ressourcen mehr übrig, um die Anfragen legitimer Besucher:innen zu bedienen.

      Die gängige Praxis ist es, die Session-ID nicht bei jeder Anfrage neu zu vergeben, sondern nur bei Änderungen der Nutzer-Priviligen, zum Beispiel wenn der Nutzer sich ein- oder ausloggt. Außerdem sollten veraltete Session-IDs ungültig gemacht werden und nach angemessener Lebensdauer automatisch verworfen werden. Auch zu diesem Thema gibt es ein Cheat Sheet von OWASP.

      Das ist aber ein anderes Thema. Ich verstehe auch nicht, wie das Erneuern der Session-ID vor CSRF-Angriffen schützen sollte.

      1. Guten Morgen in die Runde

        Ich bin echt positiv überrascht über eure Beteiligung hier im forum. Ich weiß ja selbst, dass ich noch an der Oberfläche rum kratze. Umso bemerkenswerter, dass ihr nicht lehrmeisterhaft daher kommt, wie ich das in anderen Foren erlebt habe.

        Dafür gibt's ein Daumen hoch 👍

        1unitedpower, danke für die ausführlichen Infos, das schaue ich mir in Ruhe an.