[php] PHP에서 SQL 삽입을 어떻게 방지 할 수 있습니까?

사용자 입력이 수정없이 SQL 쿼리에 삽입 되면 다음 예제와 같이 응용 프로그램이 SQL 주입에 취약 해집니다 .

$unsafe_variable = $_POST['user_input']; 

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

사용자가와 같은 것을 입력 할 수 value'); DROP TABLE table;--있고 쿼리가 다음과 같이되기 때문입니다 .

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

이 문제가 발생하지 않도록하려면 어떻게해야합니까?



답변

준비된 명령문과 매개 변수화 된 쿼리를 사용하십시오. 이는 데이터베이스 서버에서 매개 변수와 별도로 보내거나 구문 분석하는 SQL 문입니다. 이런 식으로 공격자가 악의적 인 SQL을 주입하는 것은 불가능합니다.

기본적으로이를 달성하기위한 두 가지 옵션이 있습니다.

  1. PDO 사용 (지원되는 모든 데이터베이스 드라이버) :

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute([ 'name' => $name ]);
    
    foreach ($stmt as $row) {
        // Do something with $row
    }
    
  2. MySQLi 사용 (MySQL의 경우) :

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // Do something with $row
    }
    

당신은 MySQL의 데이터베이스가 아닌 다른 데이터베이스에 연결하는 경우 (예를 들어, 참조 할 수 드라이버 별 두 번째 옵션이 pg_prepare()pg_execute()PostgreSQL을위한)가. PDO는 보편적 인 옵션입니다.


연결을 올바르게 설정

PDOMySQL 데이터베이스에 액세스 할 때 실제 준비된 명령문은 기본적으로 사용되지 않습니다 . 이 문제를 해결하려면 준비된 명령문의 에뮬레이션을 비활성화해야합니다. PDO를 사용하여 연결을 작성하는 예는 다음과 같습니다.

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

위의 예에서 오류 모드는 꼭 필요한 것은 아니지만 추가하는 것이 좋습니다 . 이런 식으로 Fatal Error무언가 잘못되었을 때 스크립트가 멈추지 않습니다 . 또한 개발자 catchthrown으로 n 인 모든 오류에 대한 기회를 제공합니다 PDOException.

무엇 필수 , 그러나, 처음으로 setAttribute()비활성화 에뮬레이트 준비된 문 및 사용에 PDO를 알려줍니다 라인, 실제 준비된 문을. 이를 통해 PHP가 명령문과 값을 구문 분석하지 않고이를 MySQL 서버로 전송합니다 (가능한 공격자에게 악의적 인 SQL을 삽입 할 기회를주지 않음).

charset생성자의 옵션에서 를 설정할 수는 있지만 PHP의 ‘이전’버전 (5.3.6 이전) 은 DSN 의 charset 매개 변수자동으로 무시했습니다 .


설명

전달한 SQL 문 prepare은 데이터베이스 서버에 의해 구문 분석되고 컴파일됩니다. 매개 변수 ( 위의 예에서 ?와 같이 또는 명명 된 매개 변수 :name)를 지정하여 데이터베이스 엔진에 필터링 할 위치를 알려줍니다. 그런 다음을 호출 execute하면 준비된 명령문이 지정한 매개 변수 값과 결합됩니다.

여기서 중요한 것은 매개 변수 값이 SQL 문자열이 아니라 컴파일 된 명령문과 결합된다는 것입니다. SQL 삽입은 데이터베이스에 보낼 SQL을 생성 할 때 악성 문자열을 포함하도록 스크립트를 속이는 방식으로 작동합니다. 따라서 실제 SQL을 매개 변수와 별도로 보내서 원하지 않는 것으로 끝나는 위험을 제한 할 수 있습니다.

준비된 명령문을 사용할 때 전송하는 모든 매개 변수는 문자열로 취급됩니다 (데이터베이스 엔진이 일부 최적화를 수행하여 매개 변수도 물론 숫자로 끝날 수 있음). 위의 예에서 $name변수 'Sarah'; DELETE FROM employees에 결과가 포함되어 있으면 단순히 문자열을 검색하는 "'Sarah'; DELETE FROM employees"것이므로 빈 테이블로 끝나지 않습니다 .

준비된 명령문을 사용하면 얻을 수있는 또 다른 이점은 동일한 세션에서 동일한 명령문을 여러 번 실행하면 구문 분석 및 컴파일이 한 번만 수행되므로 속도가 약간 향상된다는 것입니다.

아, 그리고 당신은 삽입을 위해 그것을하는 방법에 대해 물었으므로 다음은 PDO를 사용하는 예입니다.

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

$preparedStatement->execute([ 'column' => $unsafeValue ]);

준비된 명령문을 동적 쿼리에 사용할 수 있습니까?

쿼리 매개 변수에 준비된 문을 계속 사용할 수는 있지만 동적 쿼리 자체의 구조를 매개 변수화 할 수 없으며 특정 쿼리 기능을 매개 변수화 할 수 없습니다.

이러한 특정 시나리오의 경우 가장 좋은 방법은 가능한 값을 제한하는 화이트리스트 필터를 사용하는 것입니다.

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}


답변

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

보안 경고 :이 답변은 보안 모범 사례와 일치하지 않습니다. 이스케이프 SQL 주입을 방지하기 위해 불충분 , 사용 준비가 문을 대신. 아래에 요약 된 전략을 사용하십시오. (또한 mysql_real_escape_string()PHP 7에서 제거되었습니다.)

최신 버전의 PHP를 사용하는 경우 mysql_real_escape_string아래에 설명 된 옵션을 더 이상 사용할 수 없습니다 ( mysqli::escape_string현대식 이지만 ). 요즘이 mysql_real_escape_string옵션은 이전 버전의 PHP에서는 레거시 코드에만 적합합니다.


의 특수 문자를 이스케이프 처리 unsafe_variable하거나 매개 변수화 된 쿼리를 사용하는 두 가지 옵션이 있습니다. 둘 다 SQL 인젝션으로부터 보호합니다. 파라미터 화 된 쿼리는 더 나은 방법으로 간주되지만 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접근하면 아래 주석에서 다항식 으로 설명 된 문제가 발생 합니다. 정수가 따옴표로 묶이지 않기 때문에이 경우는 더 까다롭기 때문에 사용자 입력에 숫자 만 포함되어 있는지 확인하여 처리 할 수 ​​있습니다.
  • 내가 모르는 다른 경우가있을 수 있습니다. 당신은 찾을 수 발생할 수있는 미묘한 문제의 일부에 유용한 자원이다.

답변

여기에있는 모든 답변은 문제의 일부만을 다루고 있습니다. 실제로, 우리는 SQL에 동적으로 추가 할 수있는 가지 쿼리 부분이 있습니다 :-

  • 숫자
  • 식별자
  • 구문 키워드

그리고 준비된 진술은 그 중 두 가지만 다룹니다.

그러나 때로는 연산자 나 식별자를 추가하여 쿼리를 더욱 역동적으로 만들어야합니다. 따라서 다른 보호 기술이 필요합니다.

일반적으로 이러한 보호 방법은 허용 목록을 기반으로 합니다.

이 경우 모든 동적 매개 변수는 스크립트에서 하드 코딩되고 해당 세트에서 선택되어야합니다. 예를 들어, 동적 순서를 수행하려면

$orders  = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. 
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

프로세스를 쉽게하기 위해 한 줄에 모든 작업을 수행 하는 화이트리스트 도우미 기능 을 작성했습니다.

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

식별자를 보호하는 또 다른 방법이 있습니다-이스케이프하지만 더 강력하고 명시적인 접근 방식으로 화이트리스트를 고수합니다. 그러나 인용 된 식별자가있는 한 인용 부호 문자를 이스케이프 처리하여 안전하게 만들 수 있습니다. 예를 들어, mysql의 경우 기본적으로 따옴표 문자를 두 배로 늘려서 이스케이프 처리해야합니다 . 다른 DBMS 이스케이프 규칙은 다를 수 있습니다.

그럼에도 불구하고,이 SQL 구문 키워드에 문제가 (예이며 AND, DESC등)하지만, 화이트리스트는이 경우의 유일한 접근 방식을 보인다.

따라서 일반적인 권장 사항은 다음과 같이 표현 될 수 있습니다.

  • SQL 데이터 리터럴을 나타내는 변수 (또는 간단히 말해서 SQL 문자열 또는 숫자)는 준비된 명령문을 통해 추가해야합니다. 예외 없음.
  • SQL 키워드, 테이블 또는 필드 이름 또는 연산자와 같은 다른 쿼리 부분은 화이트리스트를 통해 필터링해야합니다.

최신 정보

SQL 주입 보호와 관련된 모범 사례에 대한 일반적인 합의가 있지만 여전히 많은 나쁜 사례가 있습니다. 그리고 그들 중 일부는 PHP 사용자의 마음에 너무 뿌리가 있습니다. 예를 들어,이 페이지 바로 위에 80 개 이상의 삭제 된 답변이 있습니다 . 모두 나쁜 품질로 인해 또는 커뮤니티에서 나쁜 오래된 기록을 홍보하여 ​​삭제되었습니다. 더 나쁜 것은, 잘못된 답변 중 일부가 삭제되지 않고 오히려 번영한다는 것입니다.

예를 들어, 수동 문자열 이스케이프를 제안 하는 두 번째로 많이 답변 된 답변을 포함하여 (1) are (2) still (3) many (4) 답변 (5) 이 있습니다.

또는 문자열 형식화의 또 다른 방법 을 제안 하고 궁극적 인 만병 통치약으로 자랑 하는 약간 더 나은 대답이 있습니다. 물론 그렇지 않습니다. 이 방법은 일반 문자열 형식보다 낫지는 않지만 모든 단점을 유지합니다. 문자열에만 적용 할 수 있으며 다른 수동 형식과 마찬가지로 본질적으로 선택적이고 의무적 인 조치이며 모든 종류의 사람의 오류가 발생하기 쉽습니다.

OWASP 또는 PHP manual 과 같은 기관에서 지원하는 매우 오래된 미신으로 인해이 모든 것이 “탈출”과 SQL 주입으로부터의 보호 사이의 평등을 보장한다고 생각합니다.

오랫동안 PHP 매뉴얼에서 언급 한 내용에 관계없이 *_escape_string데이터를 안전하게 보호 할 수 는 없었습니다. 문자열 이외의 SQL 부분에는 쓸모가 없지만 수동 이스케이프는 자동화와 반대되는 수동이기 때문에 잘못되었습니다.

그리고 OWASP는 전혀 말도 안되는 사용자 입력 을 피하는 데 중점을 둡니다 . 주입 보호와 관련하여 그러한 단어가 없어야합니다. 모든 변수는 소스에 관계없이 잠재적으로 위험합니다! 즉, 소스에 관계없이 모든 변수는 쿼리에 들어가도록 올바르게 형식화되어야합니다. 중요한 목적지입니다. 개발자가 양을 염소와 분리하기 시작하는 순간 (특정 변수가 “안전한지”생각하는 것) 재난을 향한 첫 걸음을 내딛습니다. 말할 것도없이, 문구조차도 진입 점에서 벌크 이스케이프를 제안하며, 이미 멸시, 더 이상 사용되지 않고 제거 된 매우 따옴표 기능과 유사합니다.

따라서 “이스케이프”와 달리 준비된 명령문 실제로 SQL 삽입 (해당되는 경우)으로부터 보호하는 측정입니다.


답변

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

이를 통해 SQL 삽입을 방지 할뿐만 아니라 쿼리 속도도 향상됩니다.

그리고보다는 PDO를 사용하여 mysql_, mysqli_pgsql_기능, 당신은 당신의 응용 프로그램에 더 스위치 데이터베이스 제공 업체에 가지고있는 드문에서, 데이터베이스에서 추출 조금합니다.


답변

사용 PDO및 준비된 쿼리

( $connA는 PDO객체)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();


답변

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

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

내 접근 방식 :

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

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

    이스케이프 된 문자열은 원래 길이의 2 배 크기에 대해 걱정하지 않아도됩니다 mysql_real_escape_string. PHP 를 사용하더라도 PHP는 동일한 용량을 할당 해야하기 때문 ((2*input_length)+1)입니다.

  • 이 16 진 방법은 이진 데이터를 전송할 때 종종 사용되지만 SQL 주입 공격을 방지하기 위해 모든 데이터에 사용하지 않는 이유는 없습니다. 대신 0xMySQL 함수에 데이터를 추가 하거나 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 진 방법은 종종 정수가 문자열과 같고로 이스케이프되는 SQL 주입 공격으로 사용됩니다 mysql_real_escape_string. 그런 다음 따옴표 사용을 피할 수 있습니다.

예를 들어 다음과 같이하면 :

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

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

SELECT … WHERE id = -1 information_schema.tables에서 모든 select table_name을 통합

이제 테이블 구조를 추출하십시오.

SELECT … WHERE id = -1 table_name = 0x61727469636c65 인 information_schema.column에서 모든 select column_name을 통합합니다.

그런 다음 원하는 데이터를 선택하십시오. 멋지지 않습니까?

그러나 주입 가능한 사이트의 코더가 16 진수라면 쿼리가 다음과 같이 표시되므로 주입이 불가능합니다. SELECT ... WHERE id = UNHEX('2d312075...3635')


답변

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

보안 경고 :이 답변은 보안 모범 사례와 일치하지 않습니다. 이스케이프 SQL 주입을 방지하기 위해 불충분 , 사용 준비가 문을 대신. 아래에 요약 된 전략을 사용하십시오. (또한 mysql_real_escape_string()PHP 7에서 제거되었습니다.)

중대한

SQL 주입을 방지하는 가장 좋은 방법 은 허용되는 답변 에서 알 수 있듯이 이스케이프 대신 준비된 문 을 사용하는 것입니다 .

Aura.SqlEasyDB 와 같은 라이브러리 가있어 개발자가 준비된 명령문을보다 쉽게 ​​사용할 수 있습니다. 준비된 명령문이 SQL 삽입중지하는 데 더 나은 이유에 대해 자세히 알려면 mysql_real_escape_string()우회최근에 수정 된 WordPress의 유니 코드 SQL 주입 취약점을 참조하십시오 .

주입 방지 -mysql_real_escape_string ()

PHP는 이러한 공격을 방지하기 위해 특수 제작 된 기능을 가지고 있습니다. 당신이해야 할 모든 기능을 사용하는 것 mysql_real_escape_string입니다.

mysql_real_escape_stringMySQL 쿼리에서 사용될 문자열을 취하고 모든 SQL 주입 시도가 안전하게 이스케이프 된 동일한 문자열을 반환합니다. 기본적으로 사용자가 입력 할 수있는 귀찮은 따옴표 ( ‘)를 MySQL 안전 대체 문자 인 이스케이프 된 따옴표 \’로 대체합니다.

참고 : 이 기능을 사용하려면 데이터베이스에 연결되어 있어야합니다!

// MySQL에 연결

$name_bad = "' OR 1'";

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '";

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

자세한 내용은 MySQL-SQL 주입 방지 에서 찾을 수 있습니다 .