PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


Das Problem mit HEAD Requests in PHP

with 25 comments

Ich bin gerade auf ein interessantes Verhalten gestoßen das eventuell große Probleme bereiten kann. Wahrscheinlich nur sehr wenige von euch werden das hier wissen, trotzdem ist es sehr interessant und provoziert eventuell Probleme und Sicherheitslücken.

Das hier vorgestellte Verhalten ist wahrscheinlich kein Problem von PHP sondern eventuell vom Webserver, aber ich weiß es nicht genau. Vielleicht kann ja mal jemand mit Tomcat oder nginx dieses Problem nachstellen.

Wir nehmen folgendes Script als Beispiel:

<?php
file_put_contents('/tmp/outp', '1');

echo 'start';
file_put_contents('/tmp/outp', '2', FILE_APPEND);
echo 'ende';

Ich glaube ihr stimmt mir alle zu wenn ich behaupte: Das Script erstellt immer eine Datei mit dem Inhalt „12“. Es kann zum Beispiel nicht passieren dass nur „1“ in der Datei steht.

Falsch gedacht. Ein Client (Browser) kann einen HEAD Request zum Webserver(Apache) schicken, d.h. der Client ist nur an den Header-Informationen der Antwort interessiert. Was tut der Apache? Er startet das PHP Script, und bricht es bei der ersten Ausgabe ab!! Denn dann kann man mit PHP nicht mehr die Header verändern, und der Apache spart CPU etc. und stoppt das PHP-Script.

Was bedeutet das für unser Beispiel-Script? In der Datei /tmp/outp steht plötzlich nur „1“ drin. Man kann sich nun überlegen was das für seine eigene Applikation bedeutet. Man schaue sich dieses Beispiel an:

<?php
session_start();
$_SESSION['admin']=1;

if (!isset($_POST['pass']) || $_POST['pass']!='somepassword') {
    echo '<b>Wrong or empty password.</b><br>';
    $_SESSION['admin']=0;
} else {
    echo 'logged in';
}

Ich hoffe niemand programmiert so, aber ihr sehr das Problem: In der Session wird erstmal ein Flag gesetzt, das bei falschem Passwort wieder zurückgesetzt wird. Normalerweisse funktioniert das super, es sei denn jemand schickt einen HEAD Request wird mit falschem Passwort, dann wird das Script beim echo gestoppt, in der Session ist der Benutzer dann eingeloggt!

Oder stellt euch vor ihr habt ein Script, das eine CSV-Datei erstellt, zum Beispiel so:

<?php

$data = <datenbankabfrage>

foreach ($data as $d) {
    file_put_contents('/tmp/csv.csv', $d['id'] . ';' . $d['name'] . "\n", FILE_APPEND);
    echo ".";
}

Falls jemand das Script mit der Methode HEAD aufruft wird eure CSV-Datei nur eine Zeile enthalten, sprich defekt sein, euer System das von der Vollständigkeit der CSV-Datei ausgeht wird Fehler produzieren.

Was kann man gegen dieses Problem tun? Man rechnet einfach immer damit dass ein Script auch abbrechen kann (wenn der Apache abstürzt, das Netzteil durchbrennt), man nutzt Output-Buffering (ob_start() etc.) oder deaktiviert HEAD-Requests komplett.

Eventuell habt ihr weitere interessante Beispiele was dadurch kaputt gehen kann. Das ist jedenfalls ein Problem über das ich mir so noch nie Gedanken gemacht habe. Das Problem habe ich in diesem PDF gefunden.

Written by Michael Kliewe

August 25th, 2011 at 1:53 pm

Posted in PHP,Server-Software

Tagged with , ,

25 Responses to 'Das Problem mit HEAD Requests in PHP'

Subscribe to comments with RSS or TrackBack to 'Das Problem mit HEAD Requests in PHP'.

  1. Zum Glück muss man sich darüber gar keine Gedanken machen, wenn man strikt nach EVA entwickelt 😉

    Dein Beispiel mit dem Login ist aber auch gar nicht problematisch, da HEAD-Requests sich ansonsten wie GET verhalten und keine POST-Daten enthalten können. Hier kommt ein weiteres Prinzip ins Spiel an das sich sowieso jeder halten sollte: Request-Methoden wie GET und HEAD sind als „safe“ definiert und sollten keine anwendungsspezifischen Seiteneffekte haben.

    Fabian

    25 Aug 11 at 14:40

  2. Die Frage beim Session-Beispiel wäre ja auch ob die dann überhaupt weggeschrieben wird? Sinniger Weise sollte das dann ja auch alles abbrechen?

    Florian Heinze

    25 Aug 11 at 14:52

  3. Dein Beispiel mit dem Login ist aber auch gar nicht problematisch, da HEAD-Requests sich ansonsten wie GET verhalten und keine POST-Daten enthalten können

    HEAD Requests können keine POST Daten enthalten, das ist zwar richtig, trotzdem ist man bei dem Beispiel eingeloggt ohne das korrekte Passwort zu kennen, was normalerweise nicht möglich wäre wenn das Script normal durchlaufen würde.

    Request-Methoden wie GET und HEAD sind als “safe” definiert und sollten keine anwendungsspezifischen Seiteneffekte haben.

    Verstehe ich nicht ganz, wenn ich also eine csv-Datei auf meine Festplatte schreiben möchte, dann sollte das kein GET-Request sein? Wo steht das denn? Was darf ein GET-Request denn dann, nur lesende Zugriffe auf Datenbank und Dateisystem? Logging, Session, Dateien dürfen alle nicht beschrieben werden? Ich glaube das hält niemand so auseinander (außer du vielleicht, dann würde mich aber interessieren wie du das machst).

    @Florian: Werden bei einem die() oder exit() auch keine Session-Daten weggeschrieben? Bin mir grad nicht so sicher, das Login-Beispiel von oben habe ich nicht ausprobiert, ich habe nur die beiden anderen getestet. Ich weiß nicht ob dieser Abbruch vergleichbar ist mit beispielsweise einem die().

    Michael Kliewe

    25 Aug 11 at 15:47

  4. Wir prüfen sehr früh in unseren Skripten die Request-Methode und lassen nur gewünschte Methoden zu. In der Regel weisen wir dann alles außer GET oder POST mit einem 405er (Method Not Allowed) zurück.

    Mit den HEAD-Anfragen hatten wir vor einigen Jahren mal viel Debug-Spaß, als irgendein Firefox-Addon schon bei einem Mouseover-HEAD-Anfragen verschickte und wir uns über das merkwürdige Verhalten unserer Applikation wunderten.

    Dominik Pesch

    25 Aug 11 at 16:42

  5. Also wenn ich hier richtig getestet habe wird die Session nicht geschrieben. Hätte mich vom Gefühl her auch gewundert.

    Bei exit/die() wird natürlich die Session geschrieben. Ich denke der Abbruch bei so einem Head-Request ist anders als bei exit/die()

    Florian Heinze

    25 Aug 11 at 16:51

  6. @Michael Kliewe: Grundsätzlich sagt man, dass GET/HEAD requests eine Resource „Anzeigen“ und „POST/PUT“ Reqeusts eine Resource erzeugen/verändern.
    Ich halte mich strikt an dieses Prinzip: Anfragen, welche den Zusatnd des Servers verändern immer mit POST (und danach brav einen Redirect (PRG-Pattern)!!).

    Gibt natuerlich auch hier ein oder zwei Ausnahmen: Logging und Tracking soll ja auch auf „lesende“ Anfragen angewendet werden – hier aber am besten Software nutzen, die das gut kann (via Pixel o.ä.).

    ilja

    25 Aug 11 at 17:31

  7. Das ein Skript jederzeit abbrechen kann ist eigentlich schon immer klar und mir zumindest auch immer bewusst gewesen – spätestens seit man transaktionssichere Datenbanken benutzt und seitdem natürlich alle Aktionen so atomar wie nur möglich ausführen möchte …

    ilja

    25 Aug 11 at 17:33

  8. Von der Sicherheit mal abgesehen, finde ich Dominik seinen Hinweis mit dem Firefox-Addon sehr hilfreich. Denn hin und wieder kriegt man ja mal vom Kunden Fehlermeldungen die man selbst gar nicht reproduzieren kann, was eben auch mal an sowas liegen kann.

    Überhaupt ein interessantes Thema. Egal ob das Beispiel nun geht oder nicht 😉

    Alex

    25 Aug 11 at 17:34

  9. Und wie schaut es mit Outputbuffer ob_* aus? Da durften keine Probleme kommen.

    Jan

    25 Aug 11 at 18:19

  10. @Jan: RÜSCHTÜSCH! Daher hab ich auch keinen Schimmer, wo Michaels Problem ist. Ansonsten hilft als Holzhammer:

    deny from all

    Oliver

    25 Aug 11 at 22:27

  11. Toll, nochmal!

    >LimitExcept GET POST>
    deny from all
    >/LimitExcept>

    Oliver

    25 Aug 11 at 22:27

  12. F*CK!

    <LimitExcept GET POST>
    deny from all
    </LimitExcept>

    Oliver

    25 Aug 11 at 22:28

  13. @Jan und @Oliver: Ich schrieb ja dass ob_start() etc. (sprich Output Buffering) und auch die Deaktivierung von HEAD-Requests helfen, es gibt noch mehr Gegenmaßnahmen um zumindestens die Probleme grob zu umgehen. „Mein Problem“ ist dass nicht alle Output-Buffering benutzen oder HEAD-Abfragen blocken, das ist normalerweise eher die Ausnahme. Viele viele Scripte mixen Ausgabe und Verarbeitung beliebig, und was dann passiert kann man sich nun denken.

    Beispielsweise sollten Zend-Framework-Projekte (normalerweise) nicht anfällig sein, weil da die Ausgabe erst ganz am Ende gemacht wird. Trotzdem gibt es da draußen viele Scripte, die kein Output-Buffering benutzen und bei denen Probleme oder „komische Abbrüche“ auftreten KÖNNEN.
    Ich wollte auch eher das Verhalten darstellen, dass eben ein PHP-Script bei der ersten Body-Ausgabe gestoppt wird und es „eventuell Probleme geben kann“, abhängig vom Script. Das normale Wissen eines Programmierers ist ja nunmal dass ein Script entweder nicht gestartet oder komplett durchläuft (wenn der Server nicht abraucht oder es einen FATAL Error gibt), aber selbst sauber programmierte Scripte können nun eben „in der Mitte“ abbrechen.

    Michael Kliewe

    25 Aug 11 at 22:45

  14. @Michael
    Ich sag mal so: Wer solchen einen Bockmist wie das zweite Beispiel mit dem Login schreibt, der baut sich auch noch schlimmere Sachen in die Seite. 🙂

    Oliver

    25 Aug 11 at 22:49

  15. Ist das eigentlich irgendwo dokumentiert? Denn das verhalten fand ich schon spannend. Ich dachte bisher immer das bei einem HEAD-Request der HTTP-Server das ausficht ohne überhaupt PHP anzufragen. Da ist aktuell implementierte Lösung natürlich schon besser.

    Kann man das vielleicht durch php.ini Konfigurationen beeinflussen? Denn eigentlich hätte ich lieber dass das Script immer durch läuft wenn irgendwie möglich. Vielleicht hab ich am Ende des Scriptes Aufräumarbeiten etc. die so nicht stattfinden. Und wenn mir wer dann wer Trillionen HEAD-Requests schickt läuft mein Server voll 😉

    HEAD abstellen geht natürlich. Aber will ich eigentlich nicht, kann ja schon auch Sinn machen?

    Florian Heinze

    26 Aug 11 at 07:57

  16. Die ist ja auch nicht der einzige Fall in dem ein Script nicht komplett bis zu Ende ausgeführt werden kann.

    Hat man zum Beispiel einen lang laufenden Request, weil zum Beispiel auf eine Transaktion gewartet werden muss, dann kann der User auch den „Stop“ Button des Browsers klicken. Je nach PHP Einstellung kann es vorkommen das PHP den Prozess einfach mittendrin beendet.

    http://www.php.net/manual/en/features.connection-handling.php
    http://php.net/manual/de/function.ignore-user-abort.php

    Keine Ahnung ob man die Funktion ignore_user_aboort auch für den HEAD Request benutzen kann.

    Gruß Christian

    Christian Kaps

    26 Aug 11 at 10:16

  17. @Christian Kaps: SEHR INTERESSANT, Danke dafür! Das ist ja interessant, dass man das steuern kann wußte ich gar nicht. Und dass die Standardeinstellung ein Abbruch des Scripts ist wußte ich auch nicht… Whao

    Michael Kliewe

    26 Aug 11 at 10:37

  18. @ Warum GET „safe“ ist:
    Steht im HTTP/1.1-Standard: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html

    @ HEAD nicht erlauben:
    Das ist nicht besonders sinnvoll, da HEAD ja legitime Anwendungsfälle gibt und man u.U. massig Bandbreite und Ressourcen spart.

    Jannik

    26 Aug 11 at 14:48

  19. Das ist nicht besonders sinnvoll, da HEAD ja legitime Anwendungsfälle gibt und man u.U. massig Bandbreite und Ressourcen spart.

    Mir fällt da ehrlich gesagt nicht viel ein, was man nicht besser mit Programmierlogik machen sollte. Naja und in einer perfekten Welt ist Get sicher sicher. 🙂

    Oliver

    26 Aug 11 at 15:24

  20. Das Problem ist doch ein alter Hut?! Aus dem gleichen Grund kann keine Session erstellt werden, wenn man irrsinnigerweise 1. echo und 2. session_start() macht (ohne Buffer).

    Aber diese konstruierte Situation ist doch hoffentlich für keinen relevant? *schreck-lass-nach*

    Und: Wer ernsthaft darüber nachdenkt, HEAD abschalten zu wollen, der sollte dringenst noch mal in sich gehen. Ohne HEAD klappen so diverse Mechanismen nicht (etwa Cache).

    @Dominik Pesch: Lustig ist aber auch, wenn der Browser oder ein Plugin wie FireBug andauernd Prefetching machen. Ende 2008 gab es ja mal eine zeitlang ein Problem in Firebug, womit das Debuggen teilweise etwas umständlich wurde.

    knalli

    27 Aug 11 at 12:50

  21. Aus dem gleichen Grund kann keine Session erstellt werden, wenn man irrsinnigerweise 1. echo und 2. session_start() macht (ohne Buffer).

    Was hat das hiermit zu tun? Die Session kann nicht erstellt werden, weil der Body schon geschickt ist und daher kein Cookie mehr gesetzt werden kann. Das hat mit HEAD oder nicht HEAD überhaupt nichts zu tun.

    Wer ernsthaft darüber nachdenkt, HEAD abschalten zu wollen, der sollte dringenst noch mal in sich gehen. Ohne HEAD klappen so diverse Mechanismen nicht (etwa Cache).

    Öhm, nö! Auch das hat überhaupt nichts mit head zu tun. Einen HEAD Request zu cachen ist Unsinn und GET wird auch ohne HEAD gecached. Was Du vermutlich meinst ist, dass nicht jedesmal der ganze Body neu gesendet wird, aber das hat mit einem HEAD Request nichts zu tun, weil Du kannst ja in der Programmlogik einen 304 Header senden und dann abbrechen. Ausserdem sendet ja der Browser keinen HEAD Request, nur weil er den Body schon kennt. Das würde auch keinen Sinn machen. 🙂

    Oliver

    27 Aug 11 at 14:32

  22. Protokolltechnisch ist ein HEAD wie ein GET, nur eben ohne Response-Body. D.h. sofern man es serverseitig nicht unterscheidet, läuft man eben in das gleiche Problem (die erste Ausgabe finalisiert den Response-Header, und damit die fertige Antwort bei einem Request-HEAD respektive den fertigen Response-HEAD eines Response-GET/POST bei einem Request-GET/POST (und das wäre etwa das Sessioncookie)).
    Wenn also der Body serverseitig geschickt wird, kann der Head folgerichtig nicht mehr geändert werden.

    Es gibt den normalen GET mit einem Request-Head (etwa ETags If not modified), aber es gibt doch auch den echten HEAD. Letzteren benötigt man aber sicherlich nur bei größeren/langsamen Ressourcen, wo sowohl er zusätzliche Request als auch das Laden zu einem späteren Zeitpunkt entschieden werden muss. Ich hatte die beiden Szenarien allerdings oben vermischt, das stimmt. 🙁

    knalli

    27 Aug 11 at 14:42

  23. Ich stelle ungern so blöde Fragen, aber hast Du mir jetzt zugestimmt oder nicht? 😀

    Oliver

    27 Aug 11 at 14:53

  24. Ich würde ja gern ja sagen, aber irgendwie läuft’s auf ein nein hinaus.

    Ach, im Grunde ging es mir ja auch darum, dass dieses alte Problem des Datensendens vor der eigentlichen Controllerlogik eigentlich doch seit den 90er ausgestorben sein sollte. Sprich: ein Beitrag rein akademischer Natur? Was übrigens der Qualität dessen keinen Abbruch tut, die ist weiterhin gut.

    knalli

    27 Aug 11 at 15:00

  25. Wie Christian schon schreib, einige PHP Script werden ebendfalls abgebrochen, wenn auf den Stop-Button klickt. Ich denke, dass auch ignore_user_aboort dort weiter helfen kann.

    Jan

    27 Aug 11 at 15:05

Leave a Reply

You can add images to your comment by clicking here.