Sedan ett par år finns PHP 5. En an dess nyheter var att det kom ett nytt bättre gränssnitt för att ansluta till MySQL, nämligen mysqli - där i står för improved.
För något år sedan kom PHP 5.1 som har gränssnittet PDO.
Båda dessa stödjer det enda helsäkra sättet att slippa SQL-injektion, nämligen prepared statements. Trots detta fortsätter varenda forum på nätet att vara nedlusat med diskussioner om mysql-gränssnittet, det långsammaste och potentiellt osäkraste av dem alla.
Inte ens ihop med funktionen mysql_real_escape_string() är man helt säker mot SQL-injektion:
http://ilia.ws/archives/103-mysql_real_escape_string-versus-Prepared-Statements.html
magic quotes (och således också addslashes) läcker som ett såll och är synnerligen otillräckligt som skydd mot SQL-injektion. Men eftersom magic quotes ofta är aktiverat på servrar världen över kan det inte ignoreras.
Följande kod åtgärdar problemet:
if ( get_magic_quotes_gpc() && ( ! ini_get('magic_quotes_sybase') ) ) {
array_walk_recursive($_GET, 'stripslashes');
array_walk_recursive($_POST, 'stripslashes');
array_walk_recursive($_COOKIE,'stripslashes');
}
Men nu står vi utan skydd helt och hållet!
Dags att validera och filtrera indata.
Utför noggranna manuella kontroller på all indata. Skall en ålder uppges? Kontrollera att du fått en siffra. En mejladress, kontrollera att det uppfyller åtminstone minimikraven på att vara en rimlig adress. (Det är en avancerad vetenskap att verkligen kontrollera addresser...) Kontrollera att ett minimum finns av tecken när så krävs, liksom att man inte överskrider maxgränser.
Ev. kontroller på klientsidan är endast ett komplement till dessa, för att avlasta servern och skapa bättre användbarhet.
Tillåt aldrig formulärdata som inte kommit från ditt eget formulär.
Först nu är det dags att titta på databakopplingen. Här följer ett utkast enligt mitt tycke och smak.
/**
* Denna klass används för att instansiera eller hämta en databaskoppling
*
* Syftet med att använda factory-pattern (kring en singleton instans av ett PDO-objekt)
* är att:
* 1. Underlätta växlingen mellan just PDO (eller annat sätt att ansluta till databasen)
* 2. Snyggare kod i control-skripten ;-)
* 3. Ställa in alla per-connection inställningar
* Klassen keryxDB_cx fungerar enligt följande:
* 1. Om objektet redan finns, returnera det.
* 2. Annars, instansiera singleton, ställ in värden och returnera objektet.
*
* @author Lars Gunther <gunther@keryx.se>
* @copyright Lars Gunther <gunther@keryx.se>
* @license Creative Commons Attribution-Noncommercial-Share Alike 3.0 http://creativecommons.org/licenses/by-nc-sa/3.0/
* @package KeryxDB
* @filesource
*
* @todo Felhantering
* @todo Tidszon etc funkar ännu bara i MySQL
*/
class keryxDB_cx
{
/**
* Felmeddelanden
*/
const BAD_PARAMETER = 'keryxDB_cx::get anropad med felaktig parameter: %s.';
const UNSUP_DRIVER = 'Stöd saknas ännu i applikationen för denna driver: %s';
/**
* Felmeddelanden
* @var object
*/
private static $db;
/**
* Denna metod instansierar en databaskoppling
*
* Metoden kan anropas hur många gånger som helst utan att man upprepar åtgärderna
*
* Indata ('dbuser', 'dbpass', 'dbtype', 'dsn' och ev. 'dbtime') hämtas ifrån {@see keryxApp_appRegistry}
*
* @return object Databasobjekt
* @throws Exception vid fel.
*/
public static function get()
{
if ( is_null(self::$db) ) {
// Kontroll av "parametrar"
$dbtype = keryxApp_appRegistry::get('dbtype');
$dsn = keryxApp_appRegistry::get('dsn');
$dbuser = keryxApp_appRegistry::get('dbuser');
$dbpass = keryxApp_appRegistry::get('dbpass');
try {
self::$db = new PDO($dsn,$dbuser,$dbpass);
self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// @todo Stöd för alternativa drivers
if ( self::$db->getAttribute(PDO::ATTR_DRIVER_NAME) != 'mysql' ) {
throw new Exception(sprintf(self::UNSUP_DRIVER,self::$db->getAttribute(PDO::ATTR_DRIVER_NAME)));
}
// Se http://netevil.org/blog/2006/apr/using-pdo-mysql
self::$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
self::$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,true);
// Databasförbindelsens inställningar
// @todo: Delegera nedanstående till specifika metoder per DBMS
// Default exception handlers gäller från och med nu. Därför inga try-catch
// Tidszon per connection kräver MySQL 4.1 eller senare och data i MySQL:s tidszonstabeller
$dbtime = keryxApp_appRegistry::get('dbtime') ?
keryxApp_appRegistry::get('dbtime') : 'Europe/Stockholm' ;
$ts_sql = "SET time_zone = '$dbtime'";
$svar = self::$db->query($ts_sql);
// Lite inställningar för MySQL, se http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html
// Tolerera INGA fel under utveckling, bli generösare under drift.
// Mina ändringar från default: STRICT_TRANS_TABLES -> STRICT_ALL_TABLES, NO_ZERO_DATE
// NO_AUTO_CREATE_USER och NO_ENGINE_SUBSTITUTION är på som standard, men vanliga
// PHP-skript borde aldrig utföra operationer där åtgärder de reglerar förekommer
// För maximal portabilitet, överväg ANSI (ej implementerat här)
$mode_sql = "SET SESSION sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE'";
$svar = self::$db->query($mode_sql);
}
catch(Exception $e) {
// Felhantering saknas
// under experimentstadiet kan följande användas:
echo "<pre>\n"; var_dump($e); echo "</pre>\n"; exit;
}
}
return self::$db;
}
// Förhindra kloning eller instansiering
final private function __clone() { }
final private function __construct() { }
}
Som synes är det fritt fram att använda koden till icke-kommersiella projekt.
Närhelst man vill ha en databaskoppling skriver man bara:
// Om man autoladdar sina klasser så behövs inte nästa rad
require_once('keryxDB/cx.php'); // PEAR namngivningskonvention
$db = keryxDB_cx::get();
Sedan är det bara att tuta och köra med sina prepared statements.
Ett enklare exempel på uppkoppling och exempel på prepared statements finns här:
Mer info (i PDF-format) finns här:
http://images.omniti.net/omniti.com/talks/furlong-pdo-long.pdf
http://netevil.org/downloads/pdo-mysql.pdf
http://netevil.org/talks/PHP-Data-Objects.pdf
http://images.omniti.net/omniti.com/talks/pm_security_2007.pdf
Manualen:
http://php.net/pdo
Metoden closeCursor brukar vara vanligast att man missar som nybörjare.
Lars Gunther