{"id":501,"date":"2009-12-27T20:06:53","date_gmt":"2009-12-27T18:06:53","guid":{"rendered":"http:\/\/robert.kolatzek.org\/wblog\/?p=501"},"modified":"2009-12-27T20:06:53","modified_gmt":"2009-12-27T18:06:53","slug":"der-fluch-und-segen-von-php-sessions","status":"publish","type":"post","link":"https:\/\/blog.kolatzek.org\/wblog\/501\/der-fluch-und-segen-von-php-sessions","title":{"rendered":"Der Fluch und Segen von PHP-Sessions"},"content":{"rendered":"<p>Im Internet Magazin las ich vor wenigen Tagen einen sehr spannenden Artikel mit dem Thema &#8222;<a title=\"Schutz vor XSS und XSFR mit PHP\" href=\"http:\/\/internet-magazin.magnus.de\/artikel\/inhaltsverzeichnis-internet-magazin-012010.2.html\" target=\"_self\">Cross Site Request Forgery: So sch\u00fctzen Sie Ihren Server vor XSRF-Attacken<\/a>&#8222;. Zuf\u00e4llig hatte ich selber im Beruf mit sehr \u00e4hnlichen Problemen zu k\u00e4mpfen. Ich studierte die vorgeschlagenen Ideen und kam zu dem Folgenden Schluss: Ohne eine verbesserte Session-Verwaltung ist man als Programmierer nie sicher, ob es so funktionieren wird, wie man es gedacht hat. Der &#8222;Schuldige&#8220; war auch schnell gefunden &#8211; die PHP-eigene Session-Verwaltung!<\/p>\n<p><!--more-->Unser Problem war, dass man mehrere Fenster, Tabs oder gar Browser \u00f6ffnen durfte und jede von ihnen hat unter einigen URLs einen AJAX-basierten zeitgesteuerten Autosave-Mechanismus benutzt. Hat jemand eine solche Seite mehrmals ge\u00f6ffnet, konnten wir nicht sicherstellen, dass nur die aktuellsten Eingaben in der Datenbank gespeichert wurden. Vor der sinnlos erh\u00f6hten Last f\u00fcr die Datenbank abgesehen h\u00e4tte man \u00fcber die selbstverschuldete Verluste der Benutzer hinwegsehen k\u00f6nnen. Aber man will ja den Benutzern entgegenkommen&#8230; (&#8230;egal wie unerfahren und naiv diese sind.)<\/p>\n<p>Das Problem aus technischer Sicht und die L\u00f6sungsans\u00e4tze in 3 Akten :-)<\/p>\n<h2>Akt 1: Mehrere Tabs oder Fenster<\/h2>\n<p>Pro Browser wird jeweils nur eine Session verwendet, egal wie viele Fenster oder Tabs offen sind. Mit einer &#8222;Pr\u00fcfziffer&#8220;, wie sie in dem oben genannten Artikel vorgeschlagen wird, kann man zumindest einem Problem vorbeugen: dass alle AJAX-Aufrufe (die ja unbemerkt im Hintergrund zeitgesteuert) stattfinden als aktuell gelten. Jeder dieser Aufrufe erneuert die Session, so dass sie nie verf\u00e4llt. Doch welcher Aufruf von welcher Seite (chronologisch gesehen) kommt, ist dem Server egal. F\u00fcgt man hingegen die Ausgabe von <a title=\"PHP: session_id()\" href=\"http:\/\/php.net\/manual\/de\/function.session-id.php\" target=\"_blank\"><em>session_id()<\/em><\/a> zu den Variablen in der Form der Seite, hat man zumindest eine Seite (Tab oder Fenster) auf eine bestimmte Session festgelegt. Die Session-ID sollte man nat\u00fcrlich beim Erstellen einer solchen Formular-Seite immer wieder wechseln. Das erreicht man mit Hilfe von <em><a title=\"PHP: der Session neue ID zuordnen\" href=\"http:\/\/de3.php.net\/manual\/de\/function.session-regenerate-id.php\" target=\"_blank\">session_regenerate_id()<\/a> <\/em>. Wird die Formularseite im anderen Tab oder Fenster aufgerufen, ist die ID der zuerst aufgerufenen Seite nicht mehr die der aktuellen Session. Das AJAX-Autosave sendet die Session-ID-Variable aus dem Formular immer mit. Wird diese vom Server mit der ID der aktuellen Session abgegliechen, f\u00e4llt sofort auf, dass es ein Aufruf von einer veralteten Seite ist. Eine Warnmeldung f\u00fcr den Benutzer w\u00e4re als R\u00fcckmeldung sicherlich angebracht und unser Problem gel\u00f6st. (Vorausgesetzt der Autosave-Mechanismus wird ausgeschaltet und so unser Server geschont.)<\/p>\n<h2>Akt 2: Mehrere Browser<\/h2>\n<p>Sicher? Nein, nicht wirklich! Sofern es nur ein Browser ist nutzen viele Seiten dieselbe Session. Kommt ein weiterer Browser hinzu, unterh\u00e4lt dieser eine andere Session. Jetzt kann es die gleiche URL in zwei verschiedenen Browsern (und Sessions) geben. Nur was ist dann aktuell? Die Session-ID-Variable wird immer der ID der Session gleich sein (es gibt ja auch zwei davon). Es gibt nur einen Ausweg aus diesem Casus Perplexus. Man baut \/ erweitert die PHP-eigene Sessionverwaltung um und speichert zentral den User-Agent ab. Gew\u00f6hlich wird dies nur beim Login passieren. (Der alte Browser kann abgest\u00fcrzt sein oder wurde geschlossen ohne Logout &#8211; man wei\u00df es ja nie.)\u00a0 Hier kann man sicher sein, dass eine neue Session starten soll.<\/p>\n<p>Will man die PHP-Session-Verwaltung nicht ersetzen, empfiehlt es sich, den User-Agent (der Name des Browsers sozusagen) zentral &#8211; \u00fcber die Session-Grenzen hinweg &#8211; zu speichern. Eine Datenbank w\u00e4re daf\u00fcr pr\u00e4destiniert. Eine entsprechende Tabelle mit User-ID als <em>primary key<\/em> und User-Agent-Name als <em>varchar (128)<\/em> als Typ <em>MEMORY<\/em> (d.h. ohne auf die Festplatte zu speichern) ist der schnellste Weg. Session-ID hilft hier nicht weiter, da es ja mehr als eine Session gibt. Die Benutzer-ID ist entscheidend. Sendet ein AJAX-Request die Daten an den Server, braucht PHP nur noch <em>$_SERVER[&#8218;HTTP_USER_AGENT&#8216;]<\/em> mit dem f\u00fcr den Benutzer eingetragenem Wert (des letzten Logins) abzugleichen und schon ist nur ein Browser der &#8222;zul\u00e4ssige&#8220; (weil der aktuellste).<\/p>\n<h2>Akt 3: Lost in space und der Weg zur\u00fcck<\/h2>\n<p>Doch auch die beste Technik hilft nichts, wenn der DAU (gebr\u00e4uchlich f\u00fcr das Modell: &#8222;D\u00fcmmster Anzunehmender User&#8220;) am Werk ist. Dieser Loggt sich im Browser A ein und \u00f6ffnet diese bestimmte Seite, startet gleich danach den Browser B und loggt sich dort zwei Minuten sp\u00e4ter aus (oder auch nicht). Damit ist der Browser A nicht mehr zul\u00e4ssig (weil laut Datenbank nicht der aktuelle) und der Browser B (vielleicht) nicht mehr in Benutzung. Auch hier braucht man einen Rettungsanker: Einen Login-\u00e4hnlichen Mechanismus, der dem Browser A wieder seine volle Macht zur\u00fcck gibt. Damit der DAU nicht leichtfertig mit mehreren Browsern einen Session-Ping-Pong spielt, sollte er sein Passwort eingeben. Das macht ihn stutzig und nachdenklich &#8211; ja es nervt vielleicht (zurecht). Gleichzeitig sch\u00fctzt es das System, indem es einen Browser nur nach Eingabe des Passworts als den aktuellen behandelt.<\/p>\n<p>Das Fazit<\/p>\n<p>Wie wir gesehen haben, ist diese wirklich n\u00fctzliche und einfach zu benutzende Session-Verwaltung von PHP nur halb so viel wert, wenn man an solche Situationen denkt. Einerseits Segen, andererseits ein Fluch. Weiterdenken ist Pflicht!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hier erfahren Sie, wie man die PHP-Session-Verwaltung erweitert, damit nicht mehrere Requests asynchron starten und nur einer die zu speichernden Daten senden darf: egal ob mehrere Tabs \/ Fenster eines Browsers oder aus mehreren Browsern heraus.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_sitemap_exclude":false,"_sitemap_priority":"","_sitemap_frequency":"","ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"","ocean_second_sidebar":"","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"","ocean_custom_header_template":"","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"","ocean_menu_typo_font_family":"","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":"","ocean_post_oembed":"","ocean_post_self_hosted_media":"","ocean_post_video_embed":"","ocean_link_format":"","ocean_link_format_target":"self","ocean_quote_format":"","ocean_quote_format_link":"post","ocean_gallery_link_images":"on","ocean_gallery_id":[],"footnotes":""},"categories":[8],"tags":[10,82,96,126],"class_list":["post-501","post","type-post","status-publish","format-standard","hentry","category-software","tag-ajax","tag-php","tag-server","tag-web","entry"],"_links":{"self":[{"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/posts\/501","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/comments?post=501"}],"version-history":[{"count":0,"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/posts\/501\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/media?parent=501"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/categories?post=501"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.kolatzek.org\/wblog\/wp-json\/wp\/v2\/tags?post=501"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}