كيف يمكنني منع حقن SQL في PHP؟


Answers

تحذير: رمز عينة هذا الجواب (مثل رمز عينة السؤال) يستخدم ملحق mysql الخاص بـ PHP ، والذي تم إيقافه في PHP 5.5.0 وإزالته بالكامل في PHP 7.0.0.

إذا كنت تستخدم إصدارًا حديثًا من PHP ، فلن يكون خيار mysql_real_escape_string الموضح أدناه متاحًا (على الرغم من أن mysqli::escape_string مكافئ حديث). في هذه الأيام ، سيكون خيار mysql_real_escape_string منطقيًا للشفرة القديمة فقط في إصدار قديم من PHP.

لديك خياران - الهروب من الأحرف الخاصة في unsafe_variable ، أو باستخدام استعلام ذو معلمات. كلا من شأنه أن يحميك من حقن SQL. يعتبر الاستعلام ذو المعلمات أفضل الممارسات ولكنه سيتطلب تغيير ملحق mysql أحدث في PHP قبل أن تتمكن من استخدامه.

سنعمل على تغطية سلسلة التأثيرات المنخفضة أولاً.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

//Disconnect

انظر أيضًا ، تفاصيل وظيفة mysql_real_escape_string .

لاستخدام الاستعلام ذو المعلمات ، تحتاج إلى استخدام MySQLi بدلاً من وظائف MySQL . لإعادة كتابة المثال الخاص بك ، نحتاج إلى شيء مثل ما يلي.

<?php
    $mysqli = new mysqli("server", "username", "password", "database_name");

    // TODO - Check that connection was successful.

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

سوف تكون الوظيفة الرئيسية التي ترغب في قراءتها في هناك mysqli::prepare .

أيضا ، كما اقترح آخرون ، قد تجد أنه من المفيد / الأسهل أن تصعد طبقة من التجريد بشيء مثل PDO .

يرجى ملاحظة أن القضية التي سألتم عنها هي حالة بسيطة إلى حد ما وأن الحالات الأكثر تعقيدًا قد تتطلب أساليب أكثر تعقيدًا. خاصه:

  • إذا كنت ترغب في تغيير بنية SQL استنادًا إلى إدخال المستخدم ، فلن تساعد الاستعلامات ذات المعلمات ، ولا يتم تغطية الهروب المطلوب من خلال mysql_real_escape_string . في هذا النوع من الحالات ، سيكون من الأفضل لك تمرير إدخال المستخدم من خلال قائمة بيضاء لضمان السماح فقط بالقيم "الآمنة".
  • إذا كنت تستخدم أعدادًا صحيحة من إدخال المستخدم في حالة ما mysql_real_escape_string منهج mysql_real_escape_string ، فستعاني من المشكلة التي وصفتها Polynomial في التعليقات أدناه. هذه الحالة أكثر تعقيدًا لأن الأعداد الصحيحة لن تكون محصورة بعلامات اقتباس ، لذا يمكنك التعامل معها من خلال التحقق من أن إدخال المستخدم يحتوي على أرقام فقط.
  • هناك حالات أخرى من المحتمل أنني لست على علم بها. قد تجد أن this مصدر مفيد في بعض المشكلات الأكثر دقة التي يمكن أن تواجهها.
Question

إذا تم إدخال إدخال المستخدم دون تعديل في استعلام SQL ، فسيصبح التطبيق عرضة لحقن SQL ، كما في المثال التالي:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

هذا لأنه يمكن للمستخدم إدخال شيء مثل value'); DROP TABLE table;-- value'); DROP TABLE table;-- ويصبح الاستعلام:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

ما الذي يمكن فعله لمنع حدوث ذلك؟




كما ترى ، يقترح الأشخاص استخدام العبارات المعدة مسبقًا على الأكثر. هذا ليس خطأ ، ولكن عندما يتم تنفيذ الاستعلام الخاص بك مرة واحدة فقط لكل عملية ، سيكون هناك جزاء أداء طفيف.

كنت أواجه هذه المسألة ، لكنني أعتقد أني قمت بحلها بطريقة متطورة للغاية - الطريقة التي يستخدمها المتسللون لتجنب استخدام الاقتباسات. لقد استخدمت هذا بالاقتران مع البيانات المعدلة المحذوفة. يمكنني استخدامه لمنع جميع أنواع هجمات حقن SQL المحتملة.

مقاربتي:

  • إذا كنت تتوقع أن يكون الإدخال عددًا صحيحًا ، فتأكد من أنه صحيح بالفعل . في لغة من نوع متغير مثل PHP من المهم جدا . يمكنك استخدام هذا الحل البسيط جدًا على سبيل المثال: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • إذا كنت تتوقع أي شيء آخر من عرافة صحيح ذلك . إذا كنت ستعطيه ، فسوف تفلت تماما من كل المدخلات. في C / C ++ هناك وظيفة تسمى mysql_hex_string() ، في PHP يمكنك استخدام bin2hex() .

    لا تقلق من أن السلسلة المهربة سيكون لها حجم 2x من طولها الأصلي لأنه حتى لو كنت تستخدم mysql_real_escape_string ، يجب على PHP تخصيص نفس السعة ((2*input_length)+1) ، وهو نفس الشيء.

  • غالبًا ما تستخدم هذه الطريقة السداسية عند نقل البيانات الثنائية ، ولكن لا أرى أي سبب لعدم استخدامها على جميع البيانات لمنع هجمات حقن SQL. لاحظ أنه يجب عليك إرسال البيانات UNHEX 0x إلى 0x أو استخدام الدالة MySQL ، بدلاً من ذلك ، بدلاً من ذلك.

لذلك ، على سبيل المثال ، الاستعلام:

SELECT password FROM users WHERE name = 'root'

سيصبح:

SELECT password FROM users WHERE name = 0x726f6f74

أو

SELECT password FROM users WHERE name = UNHEX('726f6f74')

الهيكس هو الهروب المثالي. لا توجد وسيلة لحقن.

الفرق بين وظيفة ال UNEX والبادئة 0x

كان هناك بعض النقاش في التعليقات ، لذا أود في النهاية توضيح ذلك. هذان الأسلوبان متشابهان للغاية ، ولكنهما مختلفان بعض الشيء من بعض النواحي:

يمكن استخدام البادئة ** 0x ** لأعمدة البيانات فقط مثل char أو varchar أو text أو block أو binary أو غير ذلك .
كما أن استخدامه معقد بعض الشيء إذا كنت على وشك إدخال سلسلة فارغة. سيكون عليك استبدالها تمامًا بـ '' ، أو ستحصل على خطأ.

تعمل UNEX () على أي عمود ؛ لا داعي للقلق حول السلسلة الفارغة.

غالبًا ما تُستخدم الطرق السداسية باعتبارها هجمات

لاحظ أن هذا الأسلوب السداسي غالبًا ما يتم استخدامه كمهاجم حقن SQL حيث الأعداد الصحيحة تشبه السلاسل والفرار فقط مع mysql_real_escape_string . ثم يمكنك تجنب استخدام الاقتباسات.

على سبيل المثال ، إذا كنت تفعل شيء كهذا:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

هجوم يمكن أن يحقن لك بسهولة . ضع في اعتبارك الشفرة التالية التي تم إدخالها من البرنامج النصي الخاص بك:

SELECT ... WHERE id = -1 union all select table_name from information_schema.tables

والآن فقط استخراج هيكل الجدول:

SELECT ... WHERE id = -1 union all select column_name from information_schema.column where table_name = 0x61727469636c65

ثم اختر فقط البيانات التي تريدها. أليس هذا رائع؟

ولكن إذا كان المبرمج في موقع قابل للحقن عشريًا ، فلن يكون هناك أي حقن ممكنًا لأن الاستعلام سيبدو كما يلي: SELECT ... WHERE id = UNHEX('2d312075...3635')




The simple alternative to this problem could be solved by granting appropriate permissions in the database itself. For example: if you are using a mysql database then enter into the database through terminal or the UI provided and just follow this command:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

This will restrict the user to only get confined with the specified query's only. Remove the delete permission and so the data would never get deleted from the query fired from the php page. The second thing to do is to flush the privileges so that the mysql refreshes the permissions and updates.

FLUSH PRIVILEGES; 

more information about flush .

To see the current privileges for the user fire the following query.

select * from mysql.user where User='username';

Learn more about GRANT .




أوصي باستخدام PDO (كائنات بيانات PHP) لتشغيل استعلامات SQL ذات معلمات.

هذا لا يحمي فقط من حقن SQL ، بل يسرع أيضا الاستعلامات.

وباستخدام PDO بدلاً من mysql_ ، و pgsql_ ، ووظائف pgsql_ ، فإنك تجعل تطبيقك أكثر تجريدًا من قاعدة البيانات ، في الحدوث النادر الذي يجب عليك تبديل مزودي قواعد البيانات.




A simple way would be to use a PHP framework like CodeIgniter or Laravel which have inbuilt features like filtering and active-record so that you don't have to worry about these nuances.




I think if someone wants to use PHP and MySQL or some other dataBase server:

  1. Think about learning PDO (PHP Data Objects) – it is a database access layer providing a uniform method of access to multiple databases.
  2. Think about learning MySQLi
  3. Use native PHP functions like: strip_tags , mysql_real_escape_string() or if variable numeric, just (int)$foo . Read more about type of variables in PHP here . If you're using libraries such as PDO or MySQLi, always use PDO::quote() and mysqli_real_escape_string() .

Libraries examples:

---- PDO

----- No placeholders - ripe for SQL injection! It's bad

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- Unnamed placeholders

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- Named placeholders

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

PS :

PDO wins this battle with ease. With support for twelve different database drivers and named parameters, we can ignore the small performance loss, and get used to its API. From a security standpoint, both of them are safe as long as the developer uses them the way they are supposed to be used

But while both PDO and MySQLi are quite fast, MySQLi performs insignificantly faster in benchmarks – ~2.5% for non-prepared statements, and ~6.5% for prepared ones.

And please test every query to your database - it's a better way to prevent injection.




** Warning: the approach described in this answer only applies to very specific scenarios and isn't secure since SQL injection attacks do not only rely on being able to inject X=Y .**

If the attackers are trying to hack into the form via PHP's $_GET variable or with the URL's query string, you would be able to catch them if they're not secure.

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

Because 1=1 , 2=2 , 1=2 , 2=1 , 1+1=2 , etc... are the common questions to an SQL database of an attacker. Maybe also it's used by many hacking applications.

But you must be careful, that you must not rewrite a safe query from your site. The code above is giving you a tip, to rewrite or redirect (it depends on you) that hacking-specific dynamic query string into a page that will store the attacker's IP address , or EVEN THEIR COOKIES, history, browser, or any other sensitive information, so you can deal with them later by banning their account or contacting authorities.




الاستعلام المعتمد والتحقق من صحة الإدخال هو الطريقة المناسبة. هناك العديد من السيناريوهات التي قد تحدث تحتها حقن SQL ، على الرغم من استخدام mysql_real_escape_string() .

هذه الأمثلة عرضة لحقن SQL:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

أو

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

في كلتا الحالتين ، لا يمكنك استخدام ' لحماية التغليف.

Source : حقن SQL غير متوقع (عند الفرار غير كاف)




A good idea is to use an 'object-relational mapper' like Idiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

It not only saves you from SQL injections but from syntax errors too! Also Supports collections of models with method chaining to filter or apply actions to multiple results at once and multiple connections.




تحذير أمان : لا يتوافق هذا الجواب مع أفضل ممارسات الأمان. الهروب غير ملائم لمنع إدخال SQL ، استخدم العبارات المعدة بدلاً من ذلك. استخدم الاستراتيجية الموضحة أدناه على مسؤوليتك الخاصة. (أيضًا ، تمت إزالة mysql_real_escape_string() في PHP 7.)

يمكنك فعل شيء أساسي مثل هذا:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

هذا لن يحل كل مشكلة ، لكنه نقطة انطلاق جيدة جدًا. لقد تركت عناصر واضحة مثل التحقق من وجود المتغير والشكل (الأرقام والحروف وما إلى ذلك).




I favor stored procedures ( MySQL has had stored procedures support since 5.0 ) from a security point of view - the advantages are -

  1. Most databases (including MySQL ) enable user access to be restricted to executing stored procedures. The fine-grained security access control is useful to prevent escalation of privileges attacks. This prevents compromised applications from being able to run SQL directly against the database.
  2. They abstract the raw SQL query from the application so less information of the database structure is available to the application. This makes it harder for people to understand the underlying structure of the database and design suitable attacks.
  3. They accept only parameters, so the advantages of parameterized queries are there. Of course - IMO you still need to sanitize your input - especially if you are using dynamic SQL inside the stored procedure.

The disadvantages are -

  1. They (stored procedures) are tough to maintain and tend to multiply very quickly. This makes managing them an issue.
  2. They are not very suitable for dynamic queries - if they are built to accept dynamic code as parameters then a lot of the advantages are negated.






I've written this little function several years ago:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

This allows running statements in an one-liner C#-ish String.Format like:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

It escapes considering the variable type. If you try to parameterize table, column names, it would fail as it puts every string in quotes which is an invalid syntax.

تحديث الأمان: str_replaceسمح الإصدار السابق بحقنات بإضافة {to} رموز مميزة إلى بيانات المستخدم. preg_replace_callbackلا يتسبب هذا الإصدار في حدوث مشكلات إذا كان الاستبدال يحتوي على هذه الرموز المميزة.




For those unsure of how to use PDO (coming from the mysql_ functions), I made a very, very simple PDO wrapper that is a single file. It exists to show how easy it is to do all the common things applications need to be done. Works with PostgreSQL, MySQL, and SQLite.

Basically, read it PDO to see how to put the PDO functions to use in real life to make it simple to store and retrieve values in the format you want.

I want a single column

$count = DB::column('SELECT COUNT(*) FROM `user`);

I want an array(key => value) results (ie for making a selectbox)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

I want a single row result

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

I want an array of results

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));



Related