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

Reference: What is a perfect code sample using the MySQL extension? [closed]

提问人: 提问时间:6/1/2011 最后编辑:14 revs, 4 users 92%Pekka 웃 更新时间:7/3/2012 访问量:2810

问:

这是为了创建一个社区学习资源。目标是提供好的代码示例,这些示例不会重复在复制/粘贴PHP代码中经常发现的可怕错误。我已要求将其制作为社区维基。

这并不意味着编码竞赛。这不是要找到最快或最紧凑的查询方式,而是要提供一个好的、可读的参考,特别是对于新手。

每天都有大量问题涌入,其中使用了 Stack Overflow 上的函数系列,代码片段非常糟糕。虽然通常最好将这些人引导到 PDO,但有时这既不可能(例如继承的遗留软件),也不符合现实的期望(用户已经在他们的项目中使用它)。mysql_*

使用该库的代码的常见问题包括:mysql_*

  • 值中的 SQL 注入
  • LIMIT 子句和动态表名中的 SQL 注入
  • 无错误报告(“为什么此查询不起作用?
  • 损坏的错误报告(即,即使代码投入生产,错误也总是发生)
  • 值输出中的跨站点脚本 (XSS) 注入

让我们编写一个 PHP 代码示例,该示例使用 mySQL_* 系列函数执行以下操作:

  • 接受两个 POST 值:(数字)和(字符串)idname
  • 对表执行 UPDATE 查询,更改 ID 为的行中的列tablenamenameid
  • 失败时,优雅地退出,但仅在生产模式下显示详细错误。 就足够了;或者,使用您选择的方法trigger_error()
  • 输出消息“已更新”。$name

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

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

好评的奖励积分。

目标是使这个问题成为用户在遇到代码错误(即使它根本不是问题的焦点)或遇到失败的查询并且不知道如何修复它的问题提问者时可以链接到的资源。

要抢占 PDO 讨论的先机,请执行以下操作:

是的,通常最好将写这些问题的个人直接交给 PDO。当这是一种选择时,我们应该这样做。然而,这并不总是可能的 - 有时,提问者正在处理遗留代码,或者已经在这个库上走了很长一段路,现在不太可能改变它。此外,如果使用得当,该系列功能是完全安全的。所以这里没有“使用 PDO”的答案。mysql_*

php mysql 安全 sql 注入

评论


答:

7赞 2 revsAaron #1

我决定跳枪,放点东西。这是开始的事情。出错时引发异常。

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 6/1/2011
虽然它有效,但对于新手和复制粘贴的人来说,它太复杂了。
1赞 Sujit Agarwal 6/1/2011
@Aaron - 你读过问题和评论吗?正确阅读它们,只是复制粘贴不是这个问题的解决方案。
1赞 Yoshi 6/1/2011
关于:失败时,优雅地退出,但仅在生产模式下显示详细错误。也许在周围添加一些条件?echo $e->getMessage();
1赞 Sujit Agarwal 6/1/2011
@Aaron - 我不会提到你的答案,而是希望其他人效法。我能理解你自己的答案。
2赞 wimvds 6/1/2011
呃,这不会将$id值分配给 name 并将 $name 分配给 id 吗?除了在调用函数时调用 updateTableName 之外,:p。
3赞 5 revs, 2 users 99%OZ_ #2
/**
 * 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攻击

评论

0赞 Pekka 6/1/2011
很好,我喜欢!我也喜欢错误处理程序,但也许对于新手来说,这里太过分了 - 现在是删除它,还是将其作为“可选”移动到底部?(我编辑掉了imysqli)
0赞 OZ_ 6/1/2011
随意编辑我蹩脚的英语:)中的错误
0赞 OZ_ 6/1/2011
@Pekka我会尽量把更多的注意力放在mysql_上,现在会编辑。
0赞 Yoshi 6/1/2011
我会用 .这使得内容更具可读性,并且可能的语法错误(缺少引号和内容)更加明显。sprintf
0赞 OZ_ 6/1/2011
@Yoshi我认为这是个人喜好,绝对不是更简单。
12赞 4 revsZnarkus #3

我捅了它一刀。试图让它尽可能简单,同时仍然保持一些现实世界的便利。

处理 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赞 Markus Hedlund 6/1/2011
@Pekka 废话,修复它。谢谢!
5赞 OZ_ 6/1/2011
-1 对于 - 糟糕的建议。不能再投反对票了,否则我会为操作员投 -1。#error_reporting(~E_ALL); ... to disable error output@
0赞 Pekka 6/1/2011
@OZ_ 我能理解你关于@的观点,但为什么在生产模式下关闭错误报告不好?
0赞 Markus Hedlund 6/1/2011
@OZ_:为什么这是一个问题?在生产模式下,禁用错误输出并没有什么坏处,只要将其记录在其他地方即可。使用 @ 是隐藏令人讨厌的输出的唯一方法。请重新考虑。mysql_connect
3赞 OZ_ 6/1/2011
@Pekka,@Znarkus,这是非常糟糕的建议,因为错误处理程序并不总是会处理错误,因为并不总是这个处理程序会被编码。 - 此变体可用于生产代码中,以隐藏错误文本,并且仅在处理错误时使用。error_reporting(0);ini_set('display_errors',0)
2赞 Your Common Sense #4
<?  
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' ?>
0赞 3 revsYour Common Sense #5

看起来我的另一个答案错过了问题的目的。
(这个也不满足某些要求,但可以看出,如果不实现处理占位符的函数,就无法实现安全的解决方案,占位符是安全查询的基石)

因此,这是发布简洁解决方案的另一种尝试,以使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 中的示例案例。