提问人: 提问时间:6/1/2011 最后编辑:14 revs, 4 users 92%Pekka 웃 更新时间:7/3/2012 访问量:2810
参考:什么是使用 MySQL 扩展的完美代码示例?[关闭]
Reference: What is a perfect code sample using the MySQL extension? [closed]
问:
这是为了创建一个社区学习资源。目标是提供好的代码示例,这些示例不会重复在复制/粘贴PHP代码中经常发现的可怕错误。我已要求将其制作为社区维基。
这并不意味着编码竞赛。这不是要找到最快或最紧凑的查询方式,而是要提供一个好的、可读的参考,特别是对于新手。
每天都有大量问题涌入,其中使用了 Stack Overflow 上的函数系列,代码片段非常糟糕。虽然通常最好将这些人引导到 PDO,但有时这既不可能(例如继承的遗留软件),也不符合现实的期望(用户已经在他们的项目中使用它)。mysql_*
使用该库的代码的常见问题包括:mysql_*
- 值中的 SQL 注入
- LIMIT 子句和动态表名中的 SQL 注入
- 无错误报告(“为什么此查询不起作用?
- 损坏的错误报告(即,即使代码投入生产,错误也总是发生)
- 值输出中的跨站点脚本 (XSS) 注入
让我们编写一个 PHP 代码示例,该示例使用 mySQL_* 系列函数执行以下操作:
- 接受两个 POST 值:(数字)和(字符串)
id
name
- 对表执行 UPDATE 查询,更改 ID 为的行中的列
tablename
name
id
- 失败时,优雅地退出,但仅在生产模式下显示详细错误。 就足够了;或者,使用您选择的方法
trigger_error()
- 输出消息“已更新”。
$name
并且没有显示上面列出的任何弱点。
它应该尽可能简单。理想情况下,它不包含任何函数或类。目标不是创建复制/可粘贴库,而是显示为确保数据库查询安全而需要执行的最少操作。
好评的奖励积分。
目标是使这个问题成为用户在遇到代码错误(即使它根本不是问题的焦点)或遇到失败的查询并且不知道如何修复它的问题提问者时可以链接到的资源。
要抢占 PDO 讨论的先机,请执行以下操作:
是的,通常最好将写这些问题的个人直接交给 PDO。当这是一种选择时,我们应该这样做。然而,这并不总是可能的 - 有时,提问者正在处理遗留代码,或者已经在这个库上走了很长一段路,现在不太可能改变它。此外,如果使用得当,该系列功能是完全安全的。所以这里没有“使用 PDO”的答案。mysql_*
答:
我决定跳枪,放点东西。这是开始的事情。出错时引发异常。
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();
}
评论
echo $e->getMessage();
/**
* 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您可以创建错误,并且查找和修复它们并不容易。
例如,在某些脚本中,您将 db1 设置为数据库,但在某些函数中,您需要将 db2 设置为数据库。
调用此函数后,数据库将被切换,脚本中的所有后续查询都会被破坏或会破坏错误数据库中的一些数据(如果表名和列名重合)。
//2. names of tables and columns should be quoted by "`" symbol
某些列的名称也可以是 SQL 关键字,使用“'”符号会有所帮助。
此外,插入到查询中的所有字符串值都应用 ' 符号引用。
//always use htmlspecialchars() to sanitize user's data in output
它将帮助您防止XSS攻击。
评论
i
mysqli
)
sprintf
我捅了它一刀。试图让它尽可能简单,同时仍然保持一些现实世界的便利。
处理 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);
}
评论
#error_reporting(~E_ALL); ... to disable error output
@
mysql_connect
error_reporting(0);
ini_set('display_errors',0)
<?
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';
}
?>
形式.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' ?>
列表.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' ?>
看起来我的另一个答案错过了问题的目的。
(这个也不满足某些要求,但可以看出,如果不实现处理占位符的函数,就无法实现安全的解决方案,占位符是安全查询的基石)
因此,这是发布简洁解决方案的另一种尝试,以使mysql查询安全而方便。
这是我很久以前写的一个函数,它对我很有帮助,直到我转向基于公司标准 OOP 的解决方案。
有两个目标需要追求:安全性和易用性。
第一个是通过实现占位符实现的。
第二个是通过实现占位符和不同的结果类型来实现的。
功能肯定不理想。一些缺点是:
- 无需将字符直接放置在查询中,因为它使用的是 printf 语法。
%
- 不支持多个连接。
- 标识符没有占位符(以及许多其他方便的占位符)。
- 同样,没有标识符占位符!。 案件必须手动处理!
"ORDER BY $field"
- 当然,OOP 实现会更加灵活,它有整齐不同的方法,而不是丑陋的“模式”变量以及其他必要的方法。
然而,它很好,安全,简洁,不需要安装整个库。
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 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 中的示例案例。
评论