Odniesienie: Jaka jest idealna próbka kodu przy użyciu rozszerzenia MySQL? [zamknięte]

To jest stworzenie społecznościowego zasobu edukacyjnego. Celem jest posiadanie przykładów dobrego kodu, który nie powtarza okropnych błędów, które tak często można znaleźć w skopiowanym / wklejonym kodzie PHP. Poprosiłem o to, aby stało się Wiki społeczności.

To nie jest konkurs kodowania. nie chodzi o znalezienie najszybszego lub najbardziej kompaktowego sposobu wykonania zapytania - chodzi o zapewnienie dobrego, czytelnego odniesienia, szczególnie dla początkujących.

Każdego dnia, jest ogromny napływ pytań z naprawdę kiepskimi fragmentami kodu używającymi rodziny funkcji mysql_* Na przepełnieniu stosu. Chociaż zwykle najlepiej jest skierować te osoby w stronę PDO, czasami nie jest to możliwe (np. odziedziczone oprogramowanie) ani realistyczne oczekiwanie (użytkownicy używają go już w swoim projekcie).

Typowe problemy z kodem używającym biblioteki mysql_* to:

  • SQL injection in values
  • SQL injection w klauzulach granicznych i dynamiczne nazwy tabel
  • brak raportowania błędów ("dlaczego to zapytanie nie działa?")
  • Jeśli kod zostanie wprowadzony do produkcji, zostanie on usunięty.]}
  • Cross-Site scripting (XSS) injection in value output

Napiszmy przykładowy kod PHP, który wykonuje następujące czynności przy użyciu mySQL_* rodziny funkcji :

  • Zaakceptuj dwie wartości posta, id (numeryczna) i name (ciąg znaków)
  • wykonaj zapytanie aktualizacyjne na Tabela tablename, zmiana kolumny {[3] } w wierszu O ID id
  • W przypadku awarii, wyjdź łaskawie, ale Pokaż szczegółowy błąd tylko w trybie produkcyjnym. trigger_error() wystarczy; alternatywnie użyj wybranej metody
  • wyświetla komunikat "$name zaktualizowany."

I czy Nie wykazuje którąkolwiek ze słabych punktów wymienionych powyżej.

Powinno być tak proste, jak to tylko możliwe . Idealnie nie zawiera żadnych funkcji ani klas. Celem nie jest Utwórz bibliotekę do kopiowania/wklejania, ale aby pokazać minimum tego, co należy zrobić, aby zapytania do bazy danych były bezpieczne.

Punkty bonusowe za dobre komentarze.

Celem jest uczynienie z tego pytania zasobu, do którego użytkownik może się połączyć, gdy napotyka pytającego, który ma zły kod (nawet jeśli nie jest to w ogóle przedmiotem pytania) lub ma do czynienia z błędnym zapytaniem i nie wie, jak to naprawić.

Aby uprzedzić PDO dyskusja:

Tak, często lepiej będzie skierować osoby piszące te pytania do PDO. Jeśli jest to opcja, powinniśmy to zrobić. Nie zawsze jest to jednak możliwe - czasami pytający pracuje nad kodem starszym, lub przeszedł już długą drogę z tą biblioteką i jest mało prawdopodobne, aby go teraz zmienić. Ponadto rodzina funkcji mysql_* jest całkowicie bezpieczna, jeśli jest używana prawidłowo. Więc nie ma odpowiedzi" użyj PDO " tutaj proszę.

Author: Pekka 웃, 2011-06-01

5 answers

Moje pchnięcie. Starałem się, aby było to tak proste, jak to możliwe, przy jednoczesnym zachowaniu niektórych udogodnień w świecie rzeczywistym.

Obsługuje unicode i używa luźnego porównania dla czytelności. Bądź miły; -)

<?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);
}
 12
Author: Znarkus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-20 10:10:40

Postanowiłem rzucić broń i po prostu coś powiesić. Od czego zacząć. Rzuca wyjątek w przypadku błędu.

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();
}
 7
Author: Aaron,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-01 21:29:01
/**
 * 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);

i kilka wyjaśnień uwag:

//1. using `dbname`. is better than using mysql_select_db()

Używając mysql_select_db możesz tworzyć błędy i nie będzie łatwo je znaleźć i naprawić.
Na przykład w niektórych skryptach ustawisz db1 jako bazę danych, ale w niektórych funkcjach musisz ustawić db2 jako bazę danych.
Po wywołaniu tej funkcji, baza danych zostanie przełączona, a wszystkie następujące zapytania w skrypcie zostaną uszkodzone lub zepsują niektóre dane w niewłaściwej bazie danych (jeśli nazwy tabel i kolumn będą zbiegu).

//2. names of tables and columns should be quoted by "`" symbol 

Niektóre nazwy kolumn mogą być również słowami kluczowymi SQL, a za pomocą "`" symbol w tym pomoże.
Również wszystkie wartości łańcuchowe, wstawione do zapytania, powinny być cytowane przez ' symbol.

//always use htmlspecialchars() to sanitize user's data in output
To pomoże Ci zapobiec atakom XSS .

 3
Author: OZ_,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-06-02 08:51:41
<?  
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'; 
}  
?>

Formularz.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' ?>

Lista.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' ?>
 2
Author: Your Common Sense,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-01-15 16:49:50

Wygląda na to, że moja druga odpowiedź przeoczyła cel pytania.
(ten również nie spełnia pewnych wymagań, ale jak widać, nie można osiągnąć bezpiecznego rozwiązania bez implementacji funkcji do przetwarzania elementów zastępczych, które są podstawą bezpiecznych zapytań)

Oto kolejna próba opublikowania zwięzłego rozwiązania, aby zapytania mysql były bezpieczne, a jednocześnie poręczne.

Funkcja, którą napisałem dawno temu i służyła mi dobrze, dopóki nie przeniosłem się do korporacyjnego standardu Rozwiązanie oparte na OOP.
Do realizacji były 2 cele: bezpieczeństwo i łatwość obsługi .

Pierwszy osiągnięty poprzez implementację elementów zastępczych.
Drugi uzyskany dzięki implementacji elementów zastępczych i różnych typów wyników.

Funkcja z pewnością nie jest idealna. Niektóre wady to:

  • nie % znaki muszą być umieszczane bezpośrednio w zapytaniu, ponieważ używa ono składni printf.
  • nie obsługuje wielu połączeń.
  • no placeholder for identyfikatory (jak również wiele innych przydatnych elementów zastępczych).
  • ponownie, brak identyfikatora zastępczego!. "ORDER BY $field" sprawa musi być załatwiona ręcznie!
  • oczywiĹ "cie implementacja OOP byĹ' aby o wiele bardziej elastyczna, majÄ ... c wĹ 'aĹ" ciwe róşne metody zamiast brzydkiej zmiennej "mode", a takĹźe inne niezbÄ ™ dne metody.

Jest jednak dobry, bezpieczny i zwięzły, nie ma potrzeby instalowania całej biblioteki.

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;
}
?>

Przykłady użycia

$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);

Jak widać z powyższego przykłady, główną różnicą od wszystkich kodów kiedykolwiek opublikowanych w Stackoverflow, zarówno procedury bezpieczeństwa, jak i odzyskiwania danych są zamknięte w kodzie funkcji. Tak więc, bez ręcznego wiązania, uciekania/cytowania lub odlewania, a także bez ręcznego pobierania danych.

W połączeniu z inną funkcją pomocniczą

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

Używane w ten sposób

$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
}
Może on zaspokoić niemal każdą potrzebę, w tym przykładowy przypadek z OP.]}
 0
Author: Your Common Sense,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-27 11:30:45