Debug Ausgaben im Code vergessen?
Wahrscheinlich ist es jedem schon einmal passiert: Irgendwo ist ein var_dump() oder file_put_contents(‚/tmp/blub‘) im Code vergessen worden, natürlich in einem sehr unwahrscheinlichen Programmzweig, und unverhofft kommt eine Email mit einer „komischen Ausgabe“ inklusive internen Informationen von einem Benutzer. Aua!
Doch was kann man dagegen machen? Die beste Lösung ist natürlich: Keine Debug-Ausgaben solcher Art benutzen, sondern nur mittels IDE-Debugging/Breakpoints etc. den Code debuggen. Oder aber man verwendet Zend_Log in Verbindung mit dem Firebug-Writer, dann erhält man auch im Browser Debug-Ausgaben, aber nicht störend mitten im Code, sondern im Firebug/FirePHP Addon des Firefox. Außerdem kann man dieses Plugin dann nur für die Development-Umgebung aktivieren, im Produktivbetrieb sollte der Writer natürlich abgeschaltet werden. Dann ist sichergestellt dass keine internen Informationen nach außen dringen.
$logger = new Zend_Log(); // create the file writer $formatter = new Zend_Log_Formatter_Simple('%timestamp% %priorityName% (%priority%): %message%' . PHP_EOL); $fileWriter = new Zend_Log_Writer_Stream($tempDirectory . "/logs/" . date("Y-m-d") . ".log"); $fileWriter->setFormatter($formatter); $logger->addWriter($fileWriter); if ($debug) { $firebugWriter = new Zend_Log_Writer_Firebug(); $logger->addWriter($firebugWriter); }
Damit auch sichergestellt ist dass kein Entwickler doch noch var_dumps nutzt und vergisst, schreiben wir auch gleich noch einen Unit-Test, der den Source-Code nach „var_dump“ und „file_put_contents“ durchsucht und Alarm schlägt falls die beiden Funktionen an „nicht erlaubten Stellen“ genutzt werden. Im unten stehenden Beispiel habe ich auch noch einen Test für „console.log()“ Aufrufe im Javascript, denn die können Probleme machen wenn der Browser diese Funktion nicht unterstützt.
Das Script hier ist abgespeckt und muss natürlich an die Gegebenheiten angepasst werden. Es findet zum Beispiel auch auskommentierte var_dumps, das könnte man noch optimieren.
<?php /* * This tests check in testing and stable if there are * console.log() function calls in javascript * var_dump() function calls in php * **/ class CheckDebuggingOutputTest extends PHPUnit_Framework_TestCase { public function testJavascriptConsoleLog() { if (strpos(APPLICATION_PATH, 'testing') !== false || strpos(APPLICATION_PATH, 'stable') !== false) { $errorString = ''; $jsList = file(APPLICATION_PATH . '/configs/js_includes.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($jsList as $jsFile) { $content = file(realpath(APPLICATION_PATH . '/../public/js/'.$jsFile), FILE_IGNORE_NEW_LINES); foreach ($content as $lineNumber => $line) { if (stripos($line, 'console.log') !== false) { $errorString .= 'console.log() call found in file '.$jsFile.' line '.($lineNumber+1)."\n"; } } } if (!empty($errorString)) { $this->fail($errorString); } } else { $this->markTestSkipped('Debugging output should only be checked in testing and stable branch'); } } public function testPhpVarDump() { if (strpos(APPLICATION_PATH, 'testing') !== false || strpos(APPLICATION_PATH, 'stable') !== false) { $errorString = ''; // get all files with .php or .phtml extension, except sandbox/ folder $iterator = new RecursiveDirectoryIterator(realpath(APPLICATION_PATH.'/../')); foreach (new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST) as $file) { if ($file->isFile() && strpos($file->getPathname(), '/sandbox')===false && $file->getFilename() != 'CheckDebuggingOutputTest.php' && (substr($file->getFilename(), -3) == 'php' || substr($file->getFilename(), -4) == 'phtml') ) { $content = file($file->getPathname(), FILE_IGNORE_NEW_LINES); foreach ($content as $lineNumber => $line) { if (stripos($line, 'var_dump') !== false) { $errorString .= 'var_dump() call found in file '.$file->getPathname().' line '.($lineNumber+1)."\n"; } } } } if (!empty($errorString)) { $this->fail($errorString); } } else { $this->markTestSkipped('Debugging output should only be checked in testing and stable branch'); } } }
Findet ihr, dass ein solcher Test in die Testsuite reingehört, und der Build fehlschlagen darf wegen eines solchen „Fehlers“? Wie prüft ihr, ob ihr Debug-Ausgaben im Code vergessen habt? Oder nutzt ihr vor jedem Commit/Release die „projektweite Suche“ der IDE, um nach soetwas zu suchen?
Linkpool Nummer 4
Heute nur 4 kurze Links, die es aber in sich haben:
PHP6 ist released!
Unsere geliebte XDebug-Extension wird bald kostenpflichtig
Alle IPv4 Adressen sind vergeben, das Internet muß für 24 Stunden abgeschaltet werden zum Umrüsten:
Weitere wichtige Nachrichten des Tages (unbedingt Lesen, einige wichtige Änderungen wird es geben und tolle neue Produkte!):
Adress- und Kontaktdaten als VCard exportieren
VCards sind kleine Dateien, die Personen- und Adressbuch-Daten enthalten. Diese Dateien kann man wunderbar an Emails dranhängen, oder aber als Export + Import zwischen verschiedenen Programmen (Outlook, Thunderbird, Mac OS X Addressbook etc) nutzen. Als Premium-Mitglied bei Xing kann man alle seine Kontakte dort auch exportieren und in die oben genannten Applikationen oder auch in sein Handy importieren.
Wie sieht eine VCard aus? Ungefähr so:
BEGIN:VCARD VERSION:3.0 N:Kliewe;Michael FN:Michael Kliewe ADR;TYPE=HOME,WORK:;;Am Stadtgarten 8a;Oelde;;59302 EMAIL;TYPE=HOME;TYPE=WORK:emailadresse@domain.de EMAIL;TYPE=PREF:wichtige@emailaddresse.de END:VCARD
Man sieht, das Format ist schon recht alt (1998), heutzutage würde man wahrscheinlich XML wählen. Aber es ist nun mal DER Standard, wir wollen die Adressdaten aus unserer Applikation als VCard exportieren. Dazu gibt es bereits einige vorgefertigte Klassen (Contact_Vcard_Build, PHP vCard Class 2.0, class vcard), die ich aber hier nicht nutzen möchte, da sie entweder das ältere 2.1 Format nutzen oder kein UTF-8 unterstützen. Hier eine einfache Lösung mit wenigen Zeilen Code:
<?php header("Content-type:text/x-vCard; charset=utf-8"); header("Content-Disposition: attachment; filename=vcardexport.vcf"); $first = "Michael"; $last = "Kliewe"; $email = "emailadresse@domain.de"; ?> BEGIN:VCARD VERSION:2.1 N:<?php echo($last); ?>;<?php echo($first); ?> FN:<?php echo($first); ?> <?php echo($last); ?> EMAIL;PREF;INTERNET:<?php echo($email); ?> REV:<?php echo date('Y-m-d H:i:s'); ?> END:VCARD
Das ist natürlich nur der Anfang und ein erster Test. Aber das Prinzip ist recht einfach: Jede Zeile enthält eine Eigenschaft, wie „Voller Name“, „Email-Adresse“, „Wohnadresse“, „Notizen“, „Urls“, „Fotos“, „Firma“, „Geburtstag“ und so weiter. Key und Value sind durch einen Doppelpunkt getrennt, spezielle Optionen wie beispielsweise das oben sichtbare „PREF“ oder „TYPE“ sind via Semikolon getrennt hinzuzufügen. Weitere Informationen erhält man auf der englischen Wikipedia-Seite (die deutsche ist nicht so umfangreich). Man kann auch mehrere Kontakte in einer vcf-Datei exportieren, und objektorientiert mit dem Zend Framework sieht das ganze dann ungefähr so aus:
public function exportvcardAction() { $this->_helper->layout()->disableLayout(); $this->_helper->viewRenderer->setNoRender(); $vcardString = ''; $contacts = $this->service->getAllContacts(); foreach ($contacts as $contact) { /* @var $contact App_Addressbook_Contact */ $vcardString .= $contact->getContactAsVcardString() . "\r\n"; } if (count($contacts) == 1) { $filename = str_replace(" ", "_", $contacts[0]->getFirstname().' '.$contacts[0]->getSurname()); } else { $filename = 'VCARDS'; } $this->getResponse()->setHeader('Content-Type', 'text/x-vCard; charset=utf-8'); $this->getResponse()->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'.vcf"'); $this->getResponse()->setHeader('Content-Length', strlen($vcardString)); $this->getResponse()->setHeader('Connection', 'close'); $this->getResponse()->setBody($vcardString); }
Damit werden nun also alle Kontakte iteriert und die einzelnen VCard-Strings aneinandergehängt. Wir nutzen hier die entsprechenden Funktionen, um das Layout und den ViewRenderer zu deaktiveren und alle Informationen ins Response-Objekt zu schreiben
Die Funktion getContactAsVcardString() habe ich so umgesetzt:
public function getContactAsVcardString() { $data['BEGIN'] = 'VCARD'; $data['VERSION'] = '3.0'; $data['PRODID'] = '-//company contact export Version 1//EN'; $data['REV'] = date('Y-m-d H:i:s'); $data['TZ'] = date('O'); $data['FN'] = $this->getDisplayName(); $data['N'] = $this->getSurname().';'.$this->getFirstname(); if ($this->getTitle() != '') { $data['TITLE'] = $this->getTitle(); } foreach ($this->getMailAddresses() as $mailAddress) { $data['EMAIL;TYPE=internet'] = $mailAddress; } if ($this->getNotice() != '') { $data['NOTE'] = $this->getNotice(); } $data['END'] = 'VCARD'; $exportString = ''; foreach ($data as $index => $value) { $exportString .= $index . ':' . $value . "\r\n"; } return $exportString; }
Als Ergebnis erhält man eine Datei zum Download mit allen $contacts incl. Details.
Mit PHP eine eBay-Auktion starten
eBay, wie viele andere große Webseiten und Dienste bietet Entwicklern natürlich auch eine API, mit der man leicht eigene Applikationen erstellen und eBay steuern kann. Nahezu alles, was man bei eBay auf der Webseite machen kann, kann man auch via API steuern, beispielsweise Verkaufen, Kaufen, Bieten, Bewerten, Suchen usw.
Um auf die API zuzugreifen müssen wir uns als „eBay Developer“ registrieren, um danach die Application-Keys (DEVID, AppID, CertID) zu erhalten. Wir erstellen uns erst einmal Sandbox-Keys, um unser Script in einer Testumgebung zu testen und kein Geld für die Tests bezahlen zu müssen. Wenn das Script erfolgreich läuft, kann man die Sandbox-Keys durch Production-Keys austauschen und richtig loslegen.
Da wir ja eine eBay-Auktion starten möchten, müssen wir uns noch einen User-Token erstellen, damit wir „im Auftrag eines eBay-Accounts“ arbeiten können.
Wir erstellen vorerst einen Testbenutzer für die Sandbox. Später werden wir einen Produktionsaccount (einen richtigen Account) nutzen und dafür einen User-Token erstellen.
Nachdem wir einen TESTUSER angelegt haben und für diesen Testuser ein User-Token generiert haben, können wir unser Script schreiben. Das folgende Script habe ich aus den PHP-Code-Samples abgeleitet. Dort findet man auch noch sehr viele andere Beispiele der API, die aber leider veraltet sind. So musste ich diverse Änderungen an den Beispiel-Codes machen, um es überhaupt ans Laufen zu bekommen (Paperless Zahlungsmöglichkeit hinzufügen, ReturnPolicy+ShippingDetails+DispatchTimeMax hinzufügen etc.).
Diese Klasse ist nur ein Testscript, es fehlen Setter und Getter usw., aber für Euch soll es reichen 😉
<?php $addItem = new eBayAddItem(); $addItem->callEbay(); $addItem->printResult(); class eBayAddItem { private $_siteId = 77; // default: Germany private $_environment = 'sandbox'; // toggle between sandbox and production private $_eBayApiVersion = 661; private $_call = 'AddItem'; private $_keys = array( 'production' => array( 'DEVID' => '', 'AppID' => '', 'CertID' => '', 'UserToken' => '', 'ServerUrl' => 'https://api.ebay.com/ws/api.dll' ), 'sandbox' => array( 'DEVID' => '6daxxxxxxxxxxxxxxxxxxxxxxxxxx1e4622', 'AppID' => 'Mixxxxxxxxxxxxxxxxxxxxxxxxxxxxxx930', 'CertID' => '68xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx00e', 'UserToken' => 'AgAxxxxxxxxlaaaangxxxxxxxxxxIrGgYZ', 'ServerUrl' => 'https://api.sandbox.ebay.com/ws/api.dll' ) ); private $_itemStartPrice = 1; private $_itemBuyItNowPrice = 10; private $_itemTitle = 'New article title2'; private $_itemDescription = 'Description here2'; private $_categoryId = 42511; private function _getRequestBody() { $apiValues = $this->_keys[$this->_environment]; $search = array( '%%USER_TOKEN%%', '%%EBAY_API_VERSION%%', '%%START_PRICE%%', '%%BUY_IT_NOW_PRICE%%', '%%TITLE%%', '%%DESCRIPTION%%', '%%CATEGORY_ID%%' ); $replace = array( $apiValues['UserToken'], $this->_eBayApiVersion, $this->_itemStartPrice, $this->_itemBuyItNowPrice, $this->_itemTitle, $this->_itemDescription, $this->_categoryId ); $requestXmlBody = file_get_contents('additem2.xml'); $requestXmlBody = str_replace($search, $replace, $requestXmlBody); return $requestXmlBody; } public function callEbay() { $apiValues = $this->_keys[$this->_environment]; $connection = curl_init(); curl_setopt($connection, CURLOPT_URL, $apiValues['ServerUrl']); curl_setopt($connection, CURLOPT_SSL_VERIFYPEER, 0); curl_setopt($connection, CURLOPT_SSL_VERIFYHOST, 0); $headers = array ( 'X-EBAY-API-COMPATIBILITY-LEVEL: ' . $this->_eBayApiVersion, 'X-EBAY-API-DEV-NAME: ' . $apiValues['DEVID'], 'X-EBAY-API-APP-NAME: ' . $apiValues['AppID'], 'X-EBAY-API-CERT-NAME: ' . $apiValues['CertID'], 'X-EBAY-API-CALL-NAME: ' . $this->_call, 'X-EBAY-API-SITEID: ' . $this->_siteId, ); curl_setopt($connection, CURLOPT_HTTPHEADER, $headers); curl_setopt($connection, CURLOPT_POST, 1); $requestBody = $this->_getRequestBody(); curl_setopt($connection, CURLOPT_POSTFIELDS, $requestBody); curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1); $responseXml = curl_exec($connection); curl_close($connection); $this->_responseXml = $responseXml; } public function printResult() { //Xml string is parsed and creates a DOM Document object $responseDoc = new DomDocument(); $responseDoc->loadXML($this->_responseXml); //get any error nodes $errors = $responseDoc->getElementsByTagName('Errors'); //if there are error nodes if($errors->length > 0) { echo '<P><B>eBay returned the following error(s):</B>'; //display each error //Get error code, ShortMesaage and LongMessage $code = $errors->item(0)->getElementsByTagName('ErrorCode'); $shortMsg = $errors->item(0)->getElementsByTagName('ShortMessage'); $longMsg = $errors->item(0)->getElementsByTagName('LongMessage'); //Display code and shortmessage echo '<P>', $code->item(0)->nodeValue, ' : ', str_replace(">", ">", str_replace("<", "<", $shortMsg->item(0)->nodeValue)); //if there is a long message (ie ErrorLevel=1), display it if(count($longMsg) > 0) { echo '<BR>', str_replace(">", ">", str_replace("<", "<", $longMsg->item(0)->nodeValue)); } } else { //no errors //get results nodes $responses = $responseDoc->getElementsByTagName("AddItemResponse"); foreach ($responses as $response) { $acks = $response->getElementsByTagName("Ack"); $ack = $acks->item(0)->nodeValue; echo "Ack = $ack <BR />\n"; // Success if successful $endTimes = $response->getElementsByTagName("EndTime"); $endTime = $endTimes->item(0)->nodeValue; echo "endTime = $endTime <BR />\n"; $itemIDs = $response->getElementsByTagName("ItemID"); $itemID = $itemIDs->item(0)->nodeValue; echo "itemID = $itemID <BR />\n"; $linkBase = "http://cgi.sandbox.ebay.com/ws/eBayISAPI.dll?ViewItem&item="; echo "<a href=$linkBase" . $itemID . ">$this->_itemTitle</a> <BR />"; $feeNodes = $responseDoc->getElementsByTagName('Fee'); foreach($feeNodes as $feeNode) { $feeNames = $feeNode->getElementsByTagName("Name"); if ($feeNames->item(0)) { $feeName = $feeNames->item(0)->nodeValue; $fees = $feeNode->getElementsByTagName('Fee'); // get Fee amount nested in Fee $fee = $fees->item(0)->nodeValue; if ($fee > 0.0) { if ($feeName == 'ListingFee') { printf("<B>$feeName : %.2f </B><BR>\n", $fee); } else { printf("$feeName : %.2f <BR>\n", $fee); } } } } } } } }
<?xml version="1.0" encoding="UTF-8"?> <AddItemRequest xmlns="urn:ebay:apis:eBLBaseComponents"> <RequesterCredentials> <eBayAuthToken>%%USER_TOKEN%%</eBayAuthToken> </RequesterCredentials> <DetailLevel>ReturnAll</DetailLevel> <ErrorLanguage>en_US</ErrorLanguage> <Version>%%EBAY_API_VERSION%%</Version> <Item> <BuyItNowPrice currencyID="EUR">%%BUY_IT_NOW_PRICE%%</BuyItNowPrice> <Country>DE</Country> <Currency>EUR</Currency> <Description><![CDATA[%%DESCRIPTION%%]]></Description> <DispatchTimeMax>1</DispatchTimeMax> <ListingDuration>Days_7</ListingDuration> <ListingType>Chinese</ListingType> <Location><![CDATA[Oelde, NRW]]></Location> <PaymentMethods>PayPal</PaymentMethods> <PayPalEmailAddress>mypaypal@domain.de</PayPalEmailAddress> <PrimaryCategory> <CategoryID>%%CATEGORY_ID%%</CategoryID> </PrimaryCategory> <Quantity>1</Quantity> <RegionID>0</RegionID> <ReturnPolicy> <ReturnsAcceptedOption>ReturnsNotAccepted</ReturnsAcceptedOption> </ReturnPolicy> <StartPrice>%%START_PRICE%%</StartPrice> <ShippingDetails> <ShippingServiceOptions> <ShippingService>DE_SonstigeDomestic</ShippingService> <ShippingServicePriority>1</ShippingServicePriority> <FreeShipping>true</FreeShipping> </ShippingServiceOptions> </ShippingDetails> <Site>Germany</Site> <Title><![CDATA[%%TITLE%%]]></Title> </Item> </AddItemRequest>
Eine riesen Hilfe war die API-Dokumentation zur AddItem Funktion. Dort ist ein XML-Beispiel, alle Tags sind anklickbar, man erhält für jedes Tag eine Beschreibung und die möglichen und erlaubten Werte.
Vielleicht schreibe ich bald noch einen Artikel darüber, wie man seine Auktionen überwacht (Höchstgebote auflisten) und den Auktionsgewinner automatisch per Email kontaktiert.
Buchvorstellung: PHP Sicherheit
Ein weiteres Werk von meinem Berg ist durchgearbeitet: Das 333 Seiten starke Standard-Werk zu „PHP Sicherheit“ von Christopher Kunz und Security-Guru Stefan Esser, den viele wahrscheinlich von seinem „Month of PHP Bugs“ oder der PHP-Erweiterung Suhosin kennen. Vor mir liegt die 3. Auflage aus dem Jahr 2008, und ich kann auch schon mal vorweg nehmen, dass die Themen heute noch brandaktuell sind und wahrscheinlich durch 5.3 keine gravierenden neuen Tipps dazugekommen wären.
Was erwartet man bei einem solchen Titel? Eine Liste von PHP-Core-Sicherheitslücken, Hinweise wie „vertraue keinen Usereingaben“, oder Aussagen „PHP ist garnicht so unsicher wenn man es richtig benutzt“? Ja, unter anderem, und noch viel mehr.
Das Inhaltsverzeichnis liest sich wie eine Offenbarung, wirklich alle wichtigen Schlagwörter sind vertreten und machen Lust auf Details. Zuerst gehen die Autoren auf allgemeine Sicherheitsprinzipien ein, helfen dabei wie man seinem Chef erklärt warum Sicherheit viel wert ist und Zeit sowie Geld kosten darf (und muss!). Wichtige Mailinglisten wie Full Disclosure und BugTraq werden genannt, um immer auf dem Laufenden zu sein.
Im zweiten Kapitel werden Methoden aufgezeigt, wie Angreifer Informationen über Webserver, Datenbankversionen, PHP-Versionen und -Erweiterungen, verwendete Software, Pfade usw. sammeln und ausnutzen. Im darauf folgenden Kapitel werden Möglichkeiten erläutert, wie Angreifer $_GET, $_POST, $_COOKIE und sogar $_SERVER Variablen manipulieren, um Angriffe wie „HTTP Response Splitting“, „Remote Command Execution“, „Cookie Poisioning“, „Mail-Header-Manipulation“ und vieles mehr durchführen können. Natürlich werden auch Lösungen präsentiert.
(Quelle: http://projects.webappsec.org/Web-Application-Security-Statistics#Summary)
Ein ganzes Kapitel beschäftigt sich dann mit dem Web-Sicherheitsproblem Nummer 1: Cross-Site Scripting (CSS/XSS) inklusive dem Thema „Cross-Site Request Forgery“, sowie der Nummer 3: SQL Injection. Das nächste Kapitel beschäftigt sich mit Authentisierung/Authentifizierung. Ein weiteres Kapitel umfasst das Thema Session (also u.a. Session Bruteforcing, Hijacking, Fixation). Jeweils gibt es auch Anleitungen, wie man diese Gefahren abwehrt.
Dann beginnen die Kapitel, die sich mit grundsätzlichen Lösungen zur Härtung der Installation beschäftigen: die PHP-Extension ext/filter wird erläutert, suExec, der Safe-Mode, open_basedir, Code-Sandboxing mit runkit, mod_chroot und vielen vielen Einstellungen und Modulen mehr.
Das Kapitel 11 beschäftigt sich, man hätte es fast vermisst, mit Suhosin. Installation, Konfiguration und Nutzen werden umfassend erläutert. Das letzte Kapitel bilden die Filtermodule mod_security und mod_parmguard. Doch das Buch ist noch nicht am Ende, im Anhang befinden sich noch Checklisten zu sicheren Webapplikationen, die wichtigen Optionen der php.ini zusammengefasst sowie die Liste aller aktuellen Gefahren, denen man sich stellt im Internet.
Tja, was soll ich sagen, es ging durch wie Butter. Die Grundlagen sind natürlich extrem wichtig, um die Auswirkungen zu verstehen und das Vorhandensein der Lücken in der eigenen Software prüfen zu können. Ich wette, dass dem ein oder anderen Leser beim Durcharbeiten der Satz „Oha, meine Anwendung ist dagegen anfällig“ durch den Kopf geht. Interessant finde ich aber auch die Checkliste, die jeder PHP-Programmierer (eigentlich jeder Webentwickler) bei einem (Re)Launch durchgehen sollte, solch eine Liste fehlt einfach, um sie methodisch Punkt für Punkt durchgehen zu können.
Voraussetzung zum Verständnis ist natürlich ein gewisses Vorwissen im PHP-Bereich sowie ein selbst installierter Webserver, ohne ein paar Grundlagen in den Bereichen versteht man die Auswirkungen von vielen Dingen wahrscheinlich nicht. Dieses Grundlagenbuch sollte jeder gelesen haben, wenn dann noch alle gewissenhaft die Lösungen befolgen wäre das Web um vieles sicherer. Ich überlege gerade, was ich zu meckern hätte… aber mir fällt nichts ein. Keine Rechtschreibfehler, praxisnahe Beispiele, Vollständigkeit. Bin sprachlos.
Pflichtlektüre!