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에서 분리하여 데이터가 그대로 유지되고 SQL 파서에 의해 명령어로 해석되지 않도록 하는 것입니다.올바른 형식의 데이터 부분을 사용하여 SQL 문을 만들 수 있지만 세부 정보를 완전히 이해하지 못할 경우 항상 준비된 문과 매개 변수화된 쿼리를 사용해야 합니다.데이터베이스 서버에서 매개 변수와 별도로 전송 및 구문 분석되는 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 이외의 데이터베이스에 접속하고 있는 경우는, 드라이버 고유의 2번째 옵션을 참조할 수 있습니다(예:pg_prepare()그리고.pg_execute()(PostgreSQL의 경우)를 참조해 주세요.PDO는 범용 옵션입니다.


올바른 연결 설정

PDO

PDO를 사용하여 MySQL 데이터베이스에 액세스하는 경우 real prepared 문은 기본적으로 사용되지 않습니다.이 문제를 해결하려면 준비된 문의 에뮬레이션을 비활성화해야 합니다.PDO를 사용하여 연결을 만드는 예는 다음과 같습니다.

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');  $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 

위의 예에서는 에러 모드가 반드시 필요한 것은 아니지만 추가하는 것이 좋습니다.이 방법을 통해 PDO는 모든 MySQL 오류를 사용자에게 알립니다.PDOException.

단, 의무적인 것은 첫 번째입니다.setAttribute()line: PDO에 대해 에뮬레이트된 준비된 문을 비활성화하고 실제 준비된 문을 사용하도록 지시합니다.그러면 문장 및 값이 MySQL 서버로 전송되기 전에 PHP에 의해 구문 분석되지 않습니다(가능한 공격자가 악의적인 SQL을 주입할 기회는 없습니다).

단,charset컨스트럭터의 옵션에서는 PHP의 ‘오래된’ 버전(5.3.6 이전)이 DSN의 charset 파라미터를 무시한 것에 주의해 주십시오.

미스크리

mysqli의 경우 동일한 절차를 따라야 합니다.

mysqli_report(MYSQLI_REPORT_ERROR   MYSQLI_REPORT_STRICT); // error reporting $dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test'); $dbConnection->set_charset('utf8mb4'); // 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'; } 



매개 변수화된 쿼리를 사용하려면 Mysqli 또는 PDO를 사용해야 합니다.mysqli로 예를 다시 쓰려면 다음과 같은 것이 필요합니다.

<?php mysqli_report(MYSQLI_REPORT_ERROR   MYSQLI_REPORT_STRICT); $mysqli = new mysqli("server", "username", "password", "database_name");  $variable = $_POST["user-input"]; $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)"); // "s" means the database expects a string $stmt->bind_param("s", $variable); $stmt->execute(); 

여기서 설명하는 주요 기능은 입니다.

또한 다른 사람들이 제안했듯이 PDO와 같은 으로 추상화 층을 높이는 것이 유용하거나 더 쉽다는 것을 알 수 있습니다.

문의하신 케이스는 매우 단순한 케이스이며, 보다 복잡한 케이스는 보다 복잡한 접근법이 필요할 수 있습니다.특히:

  • 사용자 입력에 따라 SQL 구조를 변경하는 경우 파라미터화된 쿼리는 도움이 되지 않으며 필요한 이스케이프는 에 의해 처리되지 않습니다.mysql_real_escape_string이런 경우 사용자의 입력을 화이트리스트에 전달하여 ‘안전한’ 값만 통과시키는 것이 좋습니다.



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

  • 번호
  • 식별자
  • 구문 키워드

그리고 준비된 진술서는 그 중 단 두 가지에 불과합니다.

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

일반적으로 이러한 보호 접근 방식은 화이트리스트에 기반합니다.

이 경우 스크립트에서 모든 다이내믹 파라미터를 하드코드하여 그 세트에서 선택해야 합니다.예를 들어, 동적 순서를 설정하려면:

$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개 이상의 삭제된 답변이 있습니다.이들 답변은 모두 품질이 나쁘거나 악질적이고 시대에 뒤떨어진 관행을 조장하기 위해 커뮤니티에 의해 삭제되었습니다.설상가상으로, 일부 오답은 삭제되지 않고 오히려 번창하고 있다.

를 들어, (2) still(3) many(4) answer(5)있습니다.여기에는 수동 스트링 이스케이프를 시사하는 두 번째로 높은 응답도 포함되어 있습니다.이것은 안전하지 않은 것으로 판명된 오래된 접근법입니다.

아니면 현악기 포맷의 또 다른 방법을 제안하고 궁극의 만병통치약이라고 자랑하는 조금 더 나은 답이 있다.물론 그렇지 않습니다.이 방법은 일반 문자열 형식과 다를 바 없지만 모든 단점을 유지합니다. 즉, 문자열에만 적용 가능하며 다른 수동 형식과 마찬가지로 기본적으로 임의이며 의무적이지 않으며 어떠한 종류의 인위적 오류도 발생하기 쉽습니다.

이 모든 것은 OWASP나 PHP 매뉴얼같은 당국으로부터 지지를 받고 있는 매우 오래된 미신 때문인 것 같습니다.이 미신은 SQL 주입으로부터 “회피”와 보호 사이의 평등을 선언합니다.

PHP 매뉴얼이 오랫동안 설명한 내용과 상관없이 데이터를 안전하게 하거나 의도한 적이 없습니다.수동 이스케이프는 문자열 이외의 SQL 부분에서는 사용할 수 없을 뿐만 아니라 자동화와는 반대로 수동이기 때문에 잘못된 것입니다.

게다가 OWASP는, 유저의 입력을 회피하는 것을 강조해, 한층 더 상황을 악화시키고 있습니다.이것은 전혀 말도 안 되는 것입니다.주입 보호의 맥락에서는 이러한 단어를 사용할 수 없습니다.모든 변수는 잠재적으로 위험합니다. 출처와 상관없이!즉, 소스에 관계없이 모든 변수는 쿼리에 넣기 위해 올바른 형식을 가져야 합니다.중요한 것은 목적지다.개발자가 양과 염소를 분리하는 순간(특정 변수가 ‘안전성’인지 아닌지를 생각하면서) 재앙을 향해 첫발을 내딛는다.이 문구조차도 이미 경멸, 비호감, 제거된 마법의 인용 기능과 유사한 입구에서의 대량 탈출을 시사하는 것은 말할 것도 없습니다.

따라서 “회피”와는 달리 준비된 문장은 SQL 주입(해당하는 경우)으로부터 보호하는 수단입니다.




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

SQL 주입으로부터 보호할 뿐만 아니라 쿼리 속도도 향상됩니다.

또한 PDO를 사용하여mysql_,mysqli_,그리고.pgsql_데이터베이스 프로바이더를 전환해야 하는 경우는 드물지만 애플리케이션을 데이터베이스에서 조금 더 추상화할 수 있습니다.




사용하다PDO질의도 준비했습니다.

($conn는 입니다.PDO오브젝트)

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