参考:什么是使用MySQL扩展的完美代码示例?

61
这是创建一个社区学习资源。目标是提供良好的代码示例,避免在复制/粘贴PHP代码时出现的可怕错误。我已请求将其制作为Community Wiki。
这并不意味着它是一场编程比赛。它不是关于寻找查询最快或最紧凑的方式 - 它是为新手提供一个好的、可读的参考。
每天,在Stack Overflow上都会有大量使用mysql_*函数族的糟糕代码片段的问题涌现。虽然通常最好引导那些人使用PDO,但有时既不可能(例如继承的遗留软件),也不是现实的期望(用户已经在他们的项目中使用它)。
使用mysql_*库的代码常见问题包括:
- 值中的SQL注入 - 在LIMIT子句和动态表名中的SQL注入 - 没有错误报告(“为什么这个查询不起作用?”) - 错误报告错误(即使将代码投入生产中,错误仍然发生) - 值输出中的跨站脚本(XSS)注入
让我们编写一个PHP代码示例,使用mySQL_*函数族执行以下操作:
  • 接受两个POST值,id(数字)和name(字符串)
  • 在名为tablename的表上执行UPDATE查询,更改ID为id的行中的name
  • 失败时要优雅地退出,但仅在生产模式下显示详细错误。 trigger_error()足以;或者使用您选择的方法
  • 输出消息“$name已更新。”

并且不显示上面列出的任何弱点。

它应该尽可能简单。理想情况下,它不包含任何函数或类。目标不是创建可复制/粘贴的库,而是显示使数据库查询变得安全所需的最小操作。

好的注释会获得额外的积分。

目标是使这个问题成为一个资源,用户可以链接到该资源,当遇到具有糟糕代码(即使这根本不是问题的重点)或遇到查询失败并不知道如何修复它的问题提问者时。

为了预先讨论PDO:

是的,将编写这些问题的人直接引导到PDO通常是更好的选择。如果可以选择,我们应该这样做。然而,并不总是可能的 - 有时候,提问者正在处理遗留代码,或者已经在使用这个库上走了很长的路程,现在不太可能改变它。此外,mysql_*函数族如果正确使用是完全安全的。因此,请不要给出“使用PDO”的答案。

5个回答

12

这是我的尝试。我尽可能地保持简单,同时仍保留了一些真实世界的便利。

处理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);
}

1
@Pekka 哎呀,修好了。谢谢! - Markus Hedlund
5
“-1 for #error_reporting(~E_ALL); ... to disable error output - bad advice.” 这是个糟糕的建议,禁用错误输出应该避免使用这种方式。“Can't downvote more, else I would -1 for @ operator.” 如果能再多投票,我会给“@”运算符投反对票。 - OZ_
@Pekka:@有什么问题吗? :O - Markus Hedlund
@Znarkus中的@符号总是抑制错误输出,这被视为不好的做法。我不理解的是对于error_reporting(~E_ALL);的批评。 - Pekka
3
@Pekka,@Znarkus,“error_reporting(0);”是非常糟糕的建议,因为不是所有的错误都会被错误处理程序处理,甚至有时候这个处理程序也没有被编码。可以在生产代码中使用“ini_set('display_errors', 0)”这种方法来隐藏错误文本,但仅当错误被处理时才能使用。 - OZ_
显示剩余8条评论

7

我决定抢先行动,只是简单地发布一些内容。这是一个开始。在出错时会抛出异常。

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();
}

3
虽然它起作用,但对于新手和复制粘贴的人来说过于复杂。 - Carlos Campderrós
1
@Aaron - 你看了问题和评论吗?仔细阅读它们,简单地复制粘贴并不是这个问题的解决方案。 - Sujit Agarwal
1
关于:_在失败时,优雅地退出,但仅在生产模式下显示详细错误。_也许可以在echo $e->getMessage();周围添加一些条件语句? - Yoshi
1
@Aaron - 我并不是提到你的回答,而是希望让其他人可以跟随。我明白这是你自己的答案。 - Sujit Agarwal
2
错误,这不会将$id的值分配给$name,$name的值分配给$id吗?除了你在函数被调用时调用updateTableName而不是updateTablenameName之外。 - wimvds
显示剩余6条评论

3
/**
 * 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关键字,使用`符号可以解决这个问题。
此外,插入查询的所有字符串值都应该用'符号引用。

//始终使用htmlspecialchars()函数来清理用户在输出中的数据
它将帮助您防止XSS攻击


很好,我喜欢它! 我也喜欢错误处理程序,但也许对于新手来说有点太难了 - 现在是否有删除它或将其移至底部作为“可选项”的选项?(我编辑了 mysqli中的 i - Pekka
随时编辑我的错误英语 :) - OZ_
@Pekka 我会更加关注mysql_,现在进行编辑。 - OZ_
我会用sprintf替换字符串拼接。这样做可以使代码更易读,同时也能更容易地发现可能出现的语法错误(如缺少引号等)。 - Yoshi
@Yoshi 我认为这是个人偏好,而且绝对不是更简单的。 - OZ_

2
<?  
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' ?>

0

看起来我的另一个答案没有达到问题的目的。
(这个也不符合某些要求,但是可以看出,如果没有实现处理占位符的函数,就无法实现安全查询的基石)

因此,这里是另一种尝试发布简洁解决方案以使mysql查询既安全又方便。

我写了一个函数很久以前,它一直为我服务,直到我转向企业标准的基于OOP的解决方案。
有两个目标要追求:安全性易用性

第一个目标通过实现占位符来实现。
第二个目标通过实现占位符和不同的结果类型来实现。

该函数肯定不是理想的。一些缺点是:

  • 不需要直接在查询中放置%字符,因为它使用printf语法。
  • 不支持多个连接。
  • 没有标识符的占位符(以及许多其他方便的占位符)。
  • 再次强调,没有标识符的占位符!必须手动处理"ORDER BY $field"的情况!
  • 当然,面向对象的实现会更加灵活,具有整洁的独立方法,而不是丑陋的“模式”变量和其他必要的方法。

尽管如此,它仍然很好、安全、简洁,无需安装整个库。

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的示例案例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接