PHP에서 SQL 삽입을 방지하려면 어떻게해야합니까?



Answers

경고 : 이 답변의 샘플 코드 (질문의 샘플 코드와 같은)는 PHP의 mysql 확장을 사용합니다.이 확장은 PHP 5.5.0에서 사용되지 않으며 PHP 7.0.0에서 완전히 제거되었습니다.

최신 버전의 PHP를 사용하고 있다면, 아래에서 설명하는 mysql_real_escape_string 옵션은 더 이상 사용할 수 없게됩니다 ( mysqli::escape_string 은 최신 기능입니다). 요즈음 mysql_real_escape_string 옵션은 구 버전 PHP의 레거시 코드에만 의미가 있습니다.

unsafe_variable 특수 문자를 이스케이프 처리하거나 매개 변수화 된 쿼리를 사용하는 두 가지 옵션이 있습니다. 둘 다 SQL 주입으로부터 당신을 보호 할 것입니다. 매개 변수화 된 쿼리는 더 나은 방법이지만 PHP를 사용하기 전에 PHP에서 더 새로운 mysql 확장으로 변경해야합니다.

먼저 영향을 덜받는 문자열을 이스케이프 처리합니다.

//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 함수의 세부 사항도 보라.

매개 변수가있는 쿼리를 사용하려면 MySQL 함수 대신 MySQLi 를 사용해야합니다. 예를 재 작성하려면 다음과 같은 것이 필요할 것입니다.

<?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 접근법을 사용하면 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;-- value'); DROP TABLE table;-- , 쿼리는 다음과 같습니다.

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

이것이 일어나지 않도록하기 위해 할 수있는 일은 무엇입니까?




매개 변수화 된 쿼리 및 입력 유효성 검사가 필요합니다. mysql_real_escape_string() 이 사용 되었더라도 SQL 주입이 발생할 수있는 시나리오는 많이 있습니다.

이러한 예제는 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 삽입 (이스 케이 핑이 충분하지 않을 때)




PDO (PHP Data Objects)를 사용하여 매개 변수가있는 SQL 쿼리를 실행하는 것이 좋습니다.

SQL 인젝션을 방지 할뿐만 아니라 쿼리 속도를 높입니다.

그리고 mysql_ , mysqli_ , pgsql_ 함수가 아닌 PDO를 사용하면 드물게 데이터베이스 공급자를 전환해야하는 상황에서 데이터베이스에서 앱을 좀 더 추상화 할 수 있습니다.







보시다시피 사람들은 준비된 문장을 최대한 사용하는 것이 좋습니다. 잘못된 것은 아니지만 쿼리가 프로세스 당 한 번만 실행 되면 약간의 성능 저하가 발생합니다.

나는이 문제에 직면 해 있었지만 해커들이 따옴표 사용을 피하는 방법과 같이 매우 정교한 방법으로 해결했다고 생각합니다. 필자는 에뮬레이트 된 준비 문과 함께이 기능을 사용했습니다. 저는 모든 종류의 가능한 SQL 주입 공격을 막기 위해 그것을 사용합니다.

내 접근 방식 :

  • 입력이 정수가 될 것으로 예상되면 실제로 정수인지 확인하십시오. PHP와 같은 변수 형 언어에서는 이것이 매우 중요합니다. sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input); 예를 들어 다음과 같이 간단하고 강력한 솔루션을 사용할 수 있습니다 sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • 당신이 정수 hex 로부터 다른 것을 기대한다면. 16 진수라면 모든 입력을 완벽하게 피할 수 있습니다. C / C ++에는 mysql_hex_string() 이라는 함수가 있습니다. PHP에서는 bin2hex() 사용할 수 있습니다.

    mysql_real_escape_string 을 사용하더라도 동일한 용량 ((2*input_length)+1) 을 할당해야하므로 이스케이프 된 문자열의 원래 길이가 2x 크기가되므로 걱정하지 마십시오.

  • 이 16 진수 메서드는 이진 데이터를 전송할 때 자주 사용되지만 SQL 주입 공격을 막기 위해 모든 데이터에 사용하지 않아도됩니다. 0x 데이터를 추가하거나 UNHEX 대신 MySQL 함수를 UNHEX 합니다.

예를 들어, 쿼리는 다음과 같습니다.

SELECT password FROM users WHERE name = 'root'

될 것입니다:

SELECT password FROM users WHERE name = 0x726f6f74

또는

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

16 진수는 완벽한 탈출구입니다. 주사 할 방법이 없어.

UNHEX 함수와 0x 접두어의 차이점

코멘트에 약간의 토론이 있었기 때문에 나는 그것을 명확하게 밝히고 싶습니다. 이 두 가지 접근법은 매우 유사하지만 몇 가지면에서 조금 다릅니다.

** 0x ** 접두어는 char, varchar, text, block, binary 등과 같은 데이터 열에 만 사용할 수 있습니다.
또한 빈 문자열을 삽입하려는 경우 약간 복잡합니다. 완전히 '' 교체해야합니다. 그렇지 않으면 오류가 발생합니다.

UNHEX ()모든 열에서 작동합니다. 빈 문자열에 대해 걱정할 필요가 없습니다.

16 진법은 종종 공격으로 사용됩니다.

이 16 진법은 정수가 문자열과 같고 mysql_real_escape_string 하여 이스케이프 처리되는 SQL 주입 공격으로 자주 사용된다는 점에 유의하십시오. 그런 다음 따옴표를 사용하지 않아도됩니다.

예를 들어, 다음과 같이하면됩니다.

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

공격은 당신을 아주 쉽게 주사 할 수 있습니다 . 스크립트에서 반환 된 다음 주입 코드를 고려하십시오.

SELECT ... where id = -1 union 모두 information_schema.tables에서 table_name을 선택합니다.

이제 테이블 구조 만 추출하면됩니다.

SELECT ... where id = -1 union 모두 information_schema.column의 column_name을 선택합니다. 여기서 table_name = 0x61727469636c65

그리고 원하는 데이터를 선택하십시오. 멋지지 않니?

그러나 주사 할 수있는 사이트의 코더가 16 진수 일 경우 쿼리는 다음과 같을 것이므로 주사는 불가능합니다 : SELECT ... WHERE id = UNHEX('2d312075...3635')




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버전에서는 {#} 토큰을 사용자 데이터에 추가하여 주입을 허용했습니다. preg_replace_callback교체가이 토큰을 포함하면 이 버전은 문제를 일으키지 않습니다.




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.



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.




보안 경고 :이 답변은 보안 모범 사례와 일치하지 않습니다. 이스케이프 처리는 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 . "')");

이것은 모든 문제를 해결하지는 못하지만 아주 좋은 디딤돌입니다. 변수의 존재, 형식 (숫자, 문자 등)을 검사하는 것과 같은 명백한 항목은 생략했습니다.




** 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.




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



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.




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.




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 .




Related