복붙노트

PDO 준비된 진술은 SQL 주입을 방지하기에 충분한가요?

PHP

PDO 준비된 진술은 SQL 주입을 방지하기에 충분한가요?

다음과 같은 코드가 있다고 가정 해 보겠습니다.

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

PDO 문서는 다음과 같이 말합니다 :

SQL 주입을 피하기 위해 필자가해야 할 일이 정말로 있습니까? 정말 쉽지?

차이가 나는 경우 MySQL을 사용할 수 있습니다. 또한 SQL 주입에 대비 한 준비 문 사용에 대해서만 궁금해합니다. 이 맥락에서, 나는 XSS 나 다른 가능한 취약점에 관심이 없다.

해결법

  1. ==============================

    1.

    짧은 대답은 '아니오'입니다. PDO는 가능한 모든 SQL 주입 공격으로부터 당신을 보호하지 않습니다. 특정 모호한 경우.

    나는 PDO에 대해 이야기하기 위해이 대답을 적용하고 있습니다 ...

    긴 대답은 그리 쉽지 않습니다. 여기에 설명 된 공격을 기반으로합니다.

    그럼, 공격을 보여줌으로써 시작합시다 ...

    $pdo->query('SET NAMES gbk');
    $var = "\xbf\x27 OR 1=1 /*";
    $query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
    $stmt = $pdo->prepare($query);
    $stmt->execute(array($var));
    

    특정 상황에서는 1 행 이상을 반환합니다. 여기서 일어나는 일을 해부 해 봅시다.

    축하합니다. PDO 준비 문을 사용하여 프로그램을 성공적으로 공격하셨습니다 ...

    이제, 에뮬레이션 된 prepared statement를 비활성화함으로써이를 방지 할 수 있다는 점은 주목할 가치가 있습니다.

    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    

    일반적으로 실제로 준비된 진술 (즉, 데이터가 쿼리와 별도의 패킷으로 전송 됨)이됩니다. 그러나 PDO는 MySQL이 기본적으로 준비 할 수없는 문법을 모방하는 것에 조용히 전념합니다 : 매뉴얼에 나열되어 있지만 적절한 서버 버전을 선택하는 것은주의하십시오.)

    여기서 문제는 SET NAMES 대신 C API의 mysql_set_charset ()을 호출하지 않았다는 것입니다. 그렇게했다면, 2006 년부터 MySQL을 사용한다면 괜찮을 것입니다.

    이전 MySQL 릴리스를 사용하고 있다면, mysql_real_escape_string ()의 버그는 클라이언트가 연결 인코딩을 올바르게 알았더라도 유효하지 않은 멀티 바이트 문자가 이스케이프 목적을위한 단일 바이트로 취급된다는 것을 의미합니다 이 공격은 여전히 ​​성공할 것입니다. 이 버그는 MySQL 4.1.20, 5.0.22 및 5.1.11에서 수정되었습니다.

    그러나 PDO가 5.3.6까지 mysql_set_charset ()에 대한 C API를 공개하지 않았기 때문에 이전 버전에서는 가능한 모든 명령에 대해이 공격을 막을 수 없었습니다!  이제는 SET NAMES 대신 DSN 매개 변수로 표시됩니다.

    앞서 말했듯이이 공격이 작동하려면 데이터베이스 연결이 취약한 문자 집합을 사용하여 인코딩되어야합니다. utf8mb4는 취약하지 않지만 모든 유니 코드 문자를 지원할 수 있습니다. 따라서 대신 유니 코드 문자를 사용할 수 있습니다. 그러나 MySQL 5.5.3부터 사용 가능합니다. 다른 대안으로 utf8도 있는데, 취약하지 않으며 Unicode Basic Multilingual Plane 전체를 지원할 수 있습니다.

    또는 NO_BACKSLASH_ESCAPES SQL 모드를 활성화 할 수 있습니다. SQL 모드는 mysql_real_escape_string ()의 동작을 변경합니다. 이 모드를 사용하면 0x27이 0x527 대신 0x2727로 바뀌므로 이스케이프 프로세스가 이전에 존재하지 않는 취약한 인코딩 (예 : 0xbf27이 여전히 0xbf27 등)에 유효한 문자를 만들 수 없습니다. 따라서 서버는 여전히 문자열을 유효하지 않은 문자열로 거부하십시오. 그러나이 SQL 모드를 사용할 때 발생할 수있는 다른 취약점에 대한 @ eggyal의 대답을 참조하십시오 (PDO에는 포함되지 않음).

    다음 예제는 안전합니다.

    mysql_query('SET NAMES utf8');
    $var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
    mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
    

    서버가 예상 utf8 때문에 ...

    mysql_set_charset('gbk');
    $var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
    mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
    

    클라이언트와 서버가 일치하도록 문자 집합을 올바르게 설정했기 때문입니다.

    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $pdo->query('SET NAMES gbk');
    $stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
    $stmt->execute(array("\xbf\x27 OR 1=1 /*"));
    

    에뮬레이트 된 준비 문을 사용 중지했기 때문입니다.

    $pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
    $stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
    $stmt->execute(array("\xbf\x27 OR 1=1 /*"));
    

    문자 세트를 올바르게 설정했기 때문입니다.

    $mysqli->query('SET NAMES gbk');
    $stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
    $param = "\xbf\x27 OR 1=1 /*";
    $stmt->bind_param('s', $param);
    $stmt->execute();
    

    MySQLi는 항상 준비된 진술을하기 때문에.

    만약 너라면:

    또는

    또는

    당신은 100 % 안전합니다.

    그렇지 않으면 PDO 준비 문을 사용하는 경우에도 취약합니다.

    나는 에뮬레이트하지 않기 위해 기본값을 변경하기 위해 천천히 패치 작업을 해왔다. 내가 겪고있는 문제는 내가 그렇게 할 때 많은 테스트가 깨지다는 것이다. 한 가지 문제는 에뮬레이션 된 준비가 실행시 구문 오류 만 발생 시키지만 실제로 준비하면 준비시 오류가 발생한다는 것입니다. 그래서 문제가 발생할 수 있습니다 (그리고 이유 테스트의 일부입니다 borking).

  2. ==============================

    2.

    준비된 문 / 매개 변수화 된 쿼리는 일반적으로 해당 문에 대한 1 차 삽입을 방지하기에 충분합니다 *. 애플리케이션의 어느 곳에서도 체크되지 않은 동적 SQL을 사용하면 여전히 2 차 주입에 취약합니다.

    2 차 주입이란 데이터가 쿼리에 포함되기 전에 데이터베이스를 통해 한 번 순환되고 훨씬 더 힘들다는 것을 의미합니다. AFAIK, 공격자가 사회 공학적으로 쉽게 작업 할 수 있기 때문에 실제 설계된 2 차 공격은 거의 볼 수 없지만, 가끔씩 2 차 버그가 생길 수 있습니다.

    나중에 쿼리에서 리터럴로 사용되는 데이터베이스에 값을 저장하도록 만들 수있는 경우 2 차 주입 공격을 수행 할 수 있습니다. 예를 들어 웹 사이트에 계정을 만들 때 다음 정보를 새 사용자 이름으로 입력한다고 가정 해 봅니다 (이 질문에 대해 MySQL DB라고 가정).

    ' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
    

    사용자 이름에 다른 제한 사항이없는 경우 준비된 명령문은 삽입시에 위의 내장 된 쿼리가 실행되지 않도록하고 데이터베이스에 값을 올바르게 저장합니다. 그러나 나중에 응용 프로그램이 데이터베이스에서 사용자 이름을 검색하고 문자열 연결을 사용하여 해당 값에 새 쿼리를 포함한다고 가정합니다. 다른 사람의 암호를 볼 수도 있습니다. 사용자 테이블의 처음 몇 개의 이름이 관리자 인 경향이 있기 때문에 방금 농장을 나눠 줄 수도 있습니다. (참고 : 암호를 일반 텍스트로 저장하지 않는 이유 중 하나입니다!)

    준비된 문은 단일 쿼리에 충분하지만 자체적으로 응용 프로그램 내에서 데이터베이스에 대한 모든 액세스가 응용 프로그램에서 사용하도록하는 메커니즘이 부족하기 때문에 전체 응용 프로그램에서 SQL 주입 공격으로부터 보호하기에 충분하지 않습니다. 안전한 코드. 그러나 코드 검토 또는 정적 분석 또는 동적 SQL 준비 문을 제한하는 ORM, 데이터 계층 또는 서비스 계층의 사용과 같은 사례가 포함될 수있는 우수한 응용 프로그램 디자인의 일부로 사용되는 것은 SQL 주입을 해결하기위한 기본 도구입니다 문제.

  3. ==============================

    3.

    아니, 항상 그렇지는 않습니다.

    사용자 입력을 쿼리 자체 내에 둘지 여부에 따라 다릅니다. 예 :

    $dbh = new PDO("blahblah");
    
    $tableToUse = $_GET['userTable'];
    
    $stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
    $stmt->execute( array(':username' => $_REQUEST['username']) );
    

    SQL 인젝션에 취약 할 수 있으며, 사용자 입력이 데이터가 아니라 식별자로 사용되기 때문에이 예제에서 준비된 구문을 사용하면 작동하지 않습니다. 정답은 다음과 같은 일종의 필터링 / 유효성 검사를 사용하는 것입니다.

    $dbh = new PDO("blahblah");
    
    $tableToUse = $_GET['userTable'];
    $allowedTables = array('users','admins','moderators');
    if (!in_array($tableToUse,$allowedTables))    
     $tableToUse = 'users';
    
    $stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
    $stmt->execute( array(':username' => $_REQUEST['username']) );
    

    참고 : PDO를 사용하여 DDL (데이터 정의 언어) 외부로 나가는 데이터를 바인딩 할 수 없습니다. 즉, 작동하지 않습니다.

    $stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');
    

    위의 이유가 작동하지 않는 이유는 DESC와 ASC가 데이터가 아니기 때문입니다. PDO는 데이터에 대해서만 벗어날 수 있습니다. 둘째, 심지어 따옴표를 붙일 수도 없습니다. 사용자가 선택한 정렬을 허용하는 유일한 방법은 수동으로 필터링하여 DESC 또는 ASC인지 확인하는 것입니다.

  4. ==============================

    4.

    예, 충분합니다. 주입 형 공격이 작동하는 방식은 어떻게 든 마치 코드 인 것처럼 데이터가 있어야하는 것을 평가하기 위해 통역사 (데이터베이스)를 얻는 것입니다. 코드와 데이터를 동일한 매체 (예 : 문자열로 쿼리를 구성 할 때)에 혼합하는 경우에만 가능합니다.

    매개 변수화 된 쿼리는 코드와 데이터를 별도로 전송하므로 작동하지 않습니다.

    당신은 여전히 ​​다른 주입 형 공격에 취약 할 수 있습니다. 예를 들어 HTML 페이지에서 데이터를 사용하는 경우 XSS 유형 공격을받을 수 있습니다.

  5. ==============================

    5.

    이것은 (일부 특정 경우에) 충분하지 않습니다! 기본적으로 PDO는 MySQL을 데이터베이스 드라이버로 사용할 때 에뮬레이트 된 준비 문을 사용합니다. MySQL과 PDO를 사용할 때는 항상 에뮬레이트 된 준비 문을 비활성화해야합니다.

    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    

    항상 수행해야하는 또 다른 작업은 데이터베이스의 올바른 인코딩을 설정하는 것입니다.

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

    또한이 관련 질문을 참조하십시오 : PHP에서 SQL 주입을 방지하려면 어떻게합니까?

    또한 단지 데이터를 표시 할 때 자신이보아야 할 것들에 대한 데이터베이스 측면에 관한 것입니다. 예 : htmlspecialchars ()를 올바른 인코딩 및 인용 스타일로 다시 사용합니다.

  6. ==============================

    6.

    개인적으로 나는 항상 사용자 입력을 신뢰할 수 없으므로 데이터에 대해 위생적인 ​​형태로 실행합니다. 단, 자리 표시 자 / 매개 변수 바인딩을 사용할 때 입력 된 데이터는 서버에 별도로 SQL 문에 전송 된 다음 함께 바인딩됩니다. 여기서 핵심은 제공된 데이터를 특정 유형 및 특정 용도로 바인드하고 SQL 문의 논리를 변경할 수있는 기회를 제거한다는 것입니다.

  7. ==============================

    7.

    html 또는 js 검사를 사용하여 SQL 인젝션 프론트 엔드를 막으려는 경우에도 프런트 엔드 검사가 "바이 패스 가능"해야합니다.

    프런트 엔드 개발 도구 (요즘에는 파이어 폭스 또는 크롬으로 빌드 됨)를 사용하여 js를 비활성화하거나 패턴을 편집 할 수 있습니다.

    따라서 SQL 삽입을 방지하기 위해 컨트롤러 내부에 입력 된 날짜 백엔드를 삭제할 수 있습니다.

    GET 및 INPUT 값을 삭제하기 위해 filter_input () 네이티브 PHP 함수를 사용하는 것이 좋습니다.

    합리적인 데이터베이스 쿼리에 대해 보안을 유지하려면 데이터 형식의 유효성을 검사하기 위해 정규 표현식을 사용하는 것이 좋습니다. preg_match ()가이 경우에 도움이 될 것입니다! 하지만 조심해! 정규식 엔진은 그렇게 가볍지 않습니다. 필요한 경우에만 사용하십시오. 그렇지 않으면 응용 프로그램 성능이 저하됩니다.

    보안에는 비용이 들지만 성능을 낭비하지 마십시오!

    쉬운 예 :

    GET에서받은 값이 99보다 작은 값인지 다시 확인하려면     if (! preg_match ( '/ [0-9] {1,2} /')) {...} 더 무겁다

    if (isset($value) && intval($value)) <99) {...}
    

    따라서 최종 답은 "아니오! PDO Prepared Statements가 모든 종류의 SQL 삽입을 막지는 못합니다"입니다. 예기치 않은 값은 피할 수 없으며 예기치 않은 연결 만 방지합니다.

  8. from https://stackoverflow.com/questions/134099/are-pdo-prepared-statements-sufficient-to-prevent-sql-injection by cc-by-sa and MIT lisence