参考:什么是使用MySQL扩展的完美代码示例?
这是创build一个社区学习资源 。 目标是要有好的代码示例,不要重复在复制/粘贴的PHP代码中经常发现的可怕错误。 我已经要求它成为社区Wiki。
这不是一个编码比赛。 这不是寻找最快或最紧凑的方法来做一个查询 – 这是提供一个良好的,可读的参考,特别是对于新手。
每天,使用Stack Overflow上的mysql_*
系列函数都会有大量的问题,其代码片段很糟糕 。 虽然通常最好把这些人引导到PDO,但是有时候这是不可能的(例如inheritance的遗留软件),也不是现实的期望(用户已经在他们的项目中使用它)。
使用mysql_*
库的代码的常见问题包括:
- SQL注入值
- LIMIT子句和dynamic表名中的SQL注入
- 没有错误报告(“为什么这个查询不起作用?”)
- 损坏的错误报告(即即使在代码投入生产时也会出现错误)
- 跨站脚本(XSS)注入值输出
让我们编写一个PHP代码示例,使用mySQL_ *系列函数执行以下操作:
- 接受两个POST值,
id
(数字)和name
(一个string) - 对表
tablename
执行UPDATE查询,使用IDid
更改行中的name
列 - 失败时,退出,但只在生产模式中显示详细的错误。
trigger_error()
就足够了; 或者使用您select的方法 - 输出消息“
$name
updated”。
并没有显示上面列出的任何弱点。
它应该尽可能简单 。 它理想上不包含任何函数或类。 目标不是创build复制/可粘贴的库,而是显示为了使数据库查询安全所需完成的工作。
奖金积分好评论。
目标是使这个问题成为用户在遇到问题提问者时可以链接的资源(即使问题根本不是问题的焦点),或者遇到失败的查询,也不会知道如何解决它。
为了抢先PDO讨论:
是的,通常把这些问题写给PDO的人最好。 当这是一个select,我们应该这样做。 然而,这并不总是可能的 – 有时候,提问者正在使用遗留代码,或者已经在这个库中使用了很长时间,现在不太可能改变它。 另外,如果正确使用, mysql_*
系列函数是完全安全的。 所以请不要在这里使用PDO。
我刺伤了它。 试图保持尽可能简单,同时仍然保持一些现实世界的便利。
处理unicode并使用宽松比较的可读性。 对人好点 ;-)
<?php header('Content-type: text/html; charset=utf-8'); error_reporting(E_ALL | E_STRICT); ini_set('display_errors', 1); // display_errors can be changed to 0 in production mode to // suppress PHP's error messages /* Can be used for testing $_POST['id'] = 1; $_POST['name'] = 'Markus'; */ $config = array( 'host' => '127.0.0.1', 'user' => 'my_user', 'pass' => 'my_pass', 'db' => 'my_database' ); # Connect and disable mysql error output $connection = @mysql_connect($config['host'], $config['user'], $config['pass']); if (!$connection) { trigger_error('Unable to connect to database: ' . mysql_error(), E_USER_ERROR); } if (!mysql_select_db($config['db'])) { trigger_error('Unable to select db: ' . mysql_error(), E_USER_ERROR); } if (!mysql_set_charset('utf8')) { trigger_error('Unable to set charset for db connection: ' . mysql_error(), E_USER_ERROR); } $result = mysql_query( 'UPDATE tablename SET name = "' . mysql_real_escape_string($_POST['name']) . '" WHERE id = "' . mysql_real_escape_string($_POST['id']) . '"' ); if ($result) { echo htmlentities($_POST['name'], ENT_COMPAT, 'utf-8') . ' updated.'; } else { trigger_error('Unable to update db: ' . mysql_error(), E_USER_ERROR); }
我决定跳枪,把东西放了。 这是一开始。 引发错误时发生exception。
function executeQuery($query, $args) { $cleaned = array_map('mysql_real_escape_string', $args); if($result = mysql_query(vsprintf($query, $cleaned))) { return $result; } else { throw new Exception('MySQL Query Error: ' . mysql_error()); } } function updateTablenameName($id, $name) { $query = "UPDATE tablename SET name = '%s' WHERE id = %d"; return executeQuery($query, array($name, $id)); } try { updateTablenameName($_POST['id'], $_POST['name']); } catch(Exception $e) { echo $e->getMessage(); exit(); }
/** * Rule #0: never trust users input! */ //sanitize integer value $id = intval($_GET['id']); //sanitize string value; $name = mysql_real_escape_string($_POST['name']); //1. using `dbname`. is better than using mysql_select_db() //2. names of tables and columns should be quoted by "`" symbol //3. each variable should be sanitized (even in LIMIT clause) $q = mysql_query("UPDATE `dbname`.`tablename` SET `name`='".$name."' WHERE `id`='".$id."' LIMIT 0,1 "); if ($q===false) { trigger_error('Error in query: '.mysql_error(), E_USER_WARNING); } else { //be careful! $name contains user's data, remember Rule #0 //always use htmlspecialchars() to sanitize user's data in output print htmlspecialchars($name).' updated'; } ######################################################################## //Example, how easily is to use set_error_handler() and trigger_error() //to control error reporting in production and dev-code //Do NOT use error_reporting(0) or error_reporting(~E_ALL) - each error //should be fixed, not muted function err_handler($errno, $errstr, $errfile, $errline) { $hanle_errors_print = E_ALL & ~E_NOTICE; //if we want to print this type of errors (other types we can just write in log-file) if ($errno & $hanle_errors_print) { //$errstr can contain user's data, so... Rule #0 print PHP_EOL.'Error ['.$errno.'] in file '.$errfile.' in line '.$errline .': '.htmlspecialchars($errstr).PHP_EOL; } //here you can write error into log-file } set_error_handler('err_handler', E_ALL & ~E_NOTICE & E_USER_NOTICE & ~E_STRICT & ~E_DEPRECATED);
以及一些评论的解释:
//1. using `dbname`. is better than using mysql_select_db()
通过使用mysql_select_db,您可以创build错误,并且find并修复错误并不容易。
例如,在某些脚本中,将db1设置为数据库,但在某些function中,需要将db2设置为数据库。
调用该函数后,数据库将被切换,脚本中的所有查询都将被破坏,或者会打乱一些错误数据库中的数据(如果表和列的名称将重合)。
//2. names of tables and columns should be quoted by "`" symbol
某些列的名称也可以是SQL关键字,使用“ ` ”符号将有助于此。
另外,插入查询的所有string值都应该用'符号引用。
//always use htmlspecialchars() to sanitize user's data in output
它会帮助你防止XSS攻击 。
<? mysql_connect(); mysql_select_db("new"); $table = "test"; if($_SERVER['REQUEST_METHOD']=='POST') { $name = mysql_real_escape_string($_POST['name']); if ($id = intval($_POST['id'])) { $query="UPDATE $table SET name='$name' WHERE id=$id"; } else { $query="INSERT INTO $table SET name='$name'"; } mysql_query($query) or trigger_error(mysql_error()." in ".$query); header("Location: http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF']); exit; } if (!isset($_GET['id'])) { $LIST=array(); $query="SELECT * FROM $table"; $res=mysql_query($query); while($row=mysql_fetch_assoc($res)) $LIST[]=$row; include 'list.php'; } else { if ($id=intval($_GET['id'])) { $query="SELECT * FROM $table WHERE id=$id"; $res=mysql_query($query); $row=mysql_fetch_assoc($res); foreach ($row as $k => $v) $row[$k]=htmlspecialchars($v); } else { $row['name']=''; $row['id']=0; } include 'form.php'; } ?>
form.php的
<? include 'tpl_top.php' ?> <form method="POST"> <input type="text" name="name" value="<?=$row['name']?>"><br> <input type="hidden" name="id" value="<?=$row['id']?>"> <input type="submit"><br> <a href="?">Return to the list</a> </form> <? include 'tpl_bottom.php' ?>
list.php的
<? include 'tpl_top.php' ?> <a href="?id=0">Add item</a> <? foreach ($LIST as $row): ?> <li><a href="?id=<?=$row['id']?>"><?=$row['name']?></a> <? endforeach ?> <? include 'tpl_bottom.php' ?>
看起来我的其他答案错过了这个问题的目的。
(这也不符合一些要求,但是可以看出,没有实现处理作为安全查询的基石的占位符的function,就不能实现安全的解决scheme)
所以,这里是另一个试图发布简洁的解决scheme,使MySQL查询安全而方便。
这是我很久以前写的一个function,直到我转向基于OOP的法人解决scheme之前,
有两个目标要追求: 安全性和易用性 。
第一个通过实现占位符实现。
第二个是通过实现占位符和不同的结果types来实现的。
这个function肯定不够理想。 一些缺点是:
- 因为它使用printf语法,所以没有
%
字符必须直接放在查询中。 - 没有支持多个连接。
- 没有占位符的标识符(以及许多其他方便的占位符)。
- 再次, 没有标识符占位符! 。
"ORDER BY $field"
情况必须手动处理! - 当然,面向对象的实现会更加灵活,具有简洁明了的方法,而不是丑陋的“模式”variables以及其他必要的方法。
但是,它是好的,安全和简洁的,不需要安装一个整个图书馆。
function dbget() { /* usage: dbget($mode, $query, $param1, $param2,...); $mode - "dimension" of result: 0 - resource 1 - scalar 2 - row 3 - array of rows */ $args = func_get_args(); if (count($args) < 2) { trigger_error("dbget: too few arguments"); return false; } $mode = array_shift($args); $query = array_shift($args); $query = str_replace("%s","'%s'",$query); foreach ($args as $key => $val) { $args[$key] = mysql_real_escape_string($val); } $query = vsprintf($query, $args); if (!$query) return false; $res = mysql_query($query); if (!$res) { trigger_error("dbget: ".mysql_error()." in ".$query); return false; } if ($mode === 0) return $res; if ($mode === 1) { if ($row = mysql_fetch_row($res)) return $row[0]; else return NULL; } $a = array(); if ($mode === 2) { if ($row = mysql_fetch_assoc($res)) return $row; } if ($mode === 3) { while($row = mysql_fetch_assoc($res)) $a[]=$row; } return $a; } ?>
用法示例
$name = dbget(1,"SELECT name FROM users WHERE id=%d",$_GET['id']); $news = dbget(3,"SELECT * FROM news WHERE title LIKE %s LIMIT %d,%d", "%$_GET[search]%",$start,$per_page);
从上面的例子可以看出,与Stackoverflow中的所有代码的主要区别在于,安全和数据检索例程都封装在函数代码中。 所以,没有手动绑定,转义/引用或转换,以及没有手动数据检索。
结合其他帮手function
function dbSet($fields,$source=array()) { $set = ''; if (!$source) $source = &$_POST; foreach ($fields as $field) { if (isset($source[$field])) { $set.="`$field`='".mysql_real_escape_string($source[$field])."', "; } } return substr($set, 0, -2); }
像这样使用
$fields = explode(" ","name surname lastname address zip phone regdate"); $_POST['regdate'] = $_POST['y']."-".$_POST['m']."-".$_POST['d']; $sql = "UPDATE $table SET ".dbSet($fields).", stamp=NOW() WHERE id=%d"; $res = dbget(0,$sql, $_POST['id']); if (!$res) { _503;//calling generic 503 error function }
它可能涵盖几乎所有的需求,包括来自OP的示例。