you should read Connection to the MYSQL using OOP first.
This article shall introduce you to Prepared Statements, which is (IMO) the ultimate weapon against SQL Injection attacks.
Prepared Statements are a kind of SQL template stored at the server. Once the template is parsed by the SQL engine, you only need to feed the data and the server sends you the appropriate results. This template approach is also the base of its security, bacause SQL injection attack exploit the SQL string by inserting SQL code where the data should be. If you separate SQL syntax and data—there’s nothing left to exploit.
The database class I’m about to introduce works similar to a Singleton class, although it does not fetches anything for you. I use one of the two database classes in PHP, that are able to do Prepared Statements: PDO (the other one is MySQLi, but I personally find PDO more versatile when it comes to fetching data). The good part about PDO is, PDO does all the hard work for you, connecting, closing connections, you don’t need to worry about that. You’re not even restricted to MySQL.
prerequisites:
don’t forget to define and include the DB_SERVER, DB_NAME, DB_USER & DB_PASS constants beforehand and to make sure the classes are loaded.
I uncommented the Exception Logging, which you can read about in Make your PHP-Errors exceptional
<?php
abstract class DB
{
/**
* @var (PDO) $PDO PDO DB object
* @var (array) $PS PDOStatement object storage array
*/
private static $PDO = NULL;
private static $PS = array();
/**
* close DB connection on script end
*
* @return (void)
*/
function __destruct()
{
self::$PDO = NULL;
}
/**
* connect to the DB using PDO in a Singleton like pattern.
*
* @return (void)
* @throws (Exception) DB connection failed
*/
public static function connect()
{
if (self::$PDO === NULL)
{
try
{
$dsn = 'mysql:host=' . DB_SERVER . ';dbname=' . DB_NAME;
self::$PDO = new PDO($dsn, DB_USER, DB_PASS);
}
catch (PDOException $pdo)
{
// ErrorLog::add($pdo, __METHOD__);
$emsg = "Failed to connect to database.";
throw new Exception($emsg, 0, $pdo);
}
}
}
/**
* create the Prepared Statements and put them in an array.
*
* @param (string) $index name of the PS
* @param (string) $sql SQL string
* @return (bool) false on PDO Error
* @throws (Exception) array index already occupied
*/
public static function prepare(
$index,
$sql
)
{
# do not overwrite an existing Statement
if (isset(self::$PS[$index]))
{
$emsg = "Index [$index] is already in use.";
throw new Exception($emsg, 1);
}
self::connect();
try
{
self::$PS[$index] = self::$PDO->prepare($sql);
}
catch (PDOException $pdo)
{
// ErrorLog::add($pdo, __METHOD__);
return false;
}
return true;
}
/**
* execute a Prepared Statement using the passed parameters.
* supported input data types: string, int, bool, null
*
* @param (string) $index name of the PS to execute
* @param (array) $param parameters for the query
* [array()]
* @return (mixed) PDOStatement,
* false on
* - connection not available
* - Prepared Statement not available
* @throws (Exception) Statement not available
* @throws (Exception) parameter could not be passed
* @throws (Exception) error in DB query
* @throws (Exception) something wrong with the result
*/
public static function execute(
$index,
Array $param = array()
)
{
if (self::$PDO === NULL or !isset(self::$PS[$index]))
{
$emsg = "Index [$index] unavailable.";
throw new Exception($emsg, 1);
}
# prepare parameters
foreach ($param as $key => $val)
{
# correct numeric input key
if (is_int($key)) ++$key;
# get PDO_PARAM_* type
$type = self::getValueType($val);
# set value
$chk = self::$PS[$index]->bindValue($key, $val, $type);
# test function return value
if (!$chk)
{
$emsg = "Parameter '$key' in [$index] failed to bind.";
throw new Exception($emsg, 2);
}
}
try
{ # finally execute statement
$chk = self::$PS[$index]->execute();
}
catch (PDOException $pdo)
{
// ErrorLog::add($pdo, __METHOD__);
$emsg = "PDO-Error while executing Prepared Statement [$index].";
throw new Exception($emsg, 3, $pdo);
}
if ($chk === false)
{
$emsg = "Result Error in Prepared Statement [$index].";
throw new Exception($emsg, 3);
}
return self::$PS[$index];
}
/**
* get a PDO_PARAM_* type for the parameter value. supported types
* are: string, integer, boolean & null (blob for PHP 6).
*
* @param (mixed) $value value whose type is to be determined
* @return (int) PDO_PARAM_*
* @throws (Exception) data type is not detectable
*/
private static function getValueType(
$value
)
{
if (is_int($value))
return PDO::PARAM_INT;
if (is_string($value) or is_numeric($value))
return PDO::PARAM_STR;
if (is_bool($value))
return PDO::PARAM_BOOL;
if (is_null($value))
return PDO::PARAM_NULL;
# if (is_binary($value)) return PDO::PARAM_LOB; // PHP 6
$emsg = "Datentyp of parameters (" . gettype($value) . ") is currently not supported.";
throw new Exception($emsg, 1);
}
}
Some comments about PDO. PDO passes data according to the specified datatype (e.g. string, int). There is a method in the DB class that does that for you, so if you pass "5" (string) to an TINYINT field, it may cause an error. This is important since all data from $_GET, $_POST, etc. are strings!
How would you use this class? An Example:
// a query, which could be used to get user preferences
try
{
$sql = "
SELECT `user_pref_color`, `user_pref_bgcolor`
FROM `user_preferences`
WHERE `user_name` = :name AND `user_pw_hash` = :pass";
// prepare statement
DB::prepare("user_prefs", $sql);
// user input data
$name = $_POST['user'];
$hash = hash('ripemd160', $_POST['password']);
// query the DB
$stmt = DB::execute("user_prefs", array(":pass" => $hash, ":name" => $name));
# you see right, there is no input validation. the data are directly passed to the
# DB class. NEVER EVER do that for a standard SQL query, really.
// the following code comes from PDO handling and I can cover that in another article.
$stmt->setFetchMode(PDO::FETCH_ASSOC);
$row = $stmt->fetch();
// now you have $row['user_pref_color'] and $row['user_pref_bgcolor'] at your service
}
catch (Exception $e)
{
// notify the user that there was an error, but keep the details to you.
echo '<p>There was an Error while executing your task. We will handle the problem in due time.</p>';
// here should follow Exception logging, if you’ve not done it yet.
}
You may have noted that I don’t test explicitly, whether there is a result that I can fetch … I’ve already done that—in the DB::execute() method.
Alltogether this class helps you securing and cleaning up your code.
What remains is to wish you a happy coding.






MultiQuote











|