짧은 대답은 NO입니다 . PDO는 모든 가능한 SQL-Injection 공격으로부터 사용자를 방어하지는 않습니다. 특정 모호한 경우.
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));
특정 상황에서는 둘 이상의 행이 반환됩니다. 여기서 무슨 일이 일어나고 있는지 해부하자 :
문자 세트 선택
$pdo->query('SET NAMES gbk');
작업이 공격을 위해, 우리는 서버의 두 인코딩에 대한 연결을 기대하는 인코딩을 필요 '
ASCII의 예에서와 같이 0x27
하고 , 그 최종 바이트는 ASCII 일부 문자를 가지고 \
즉 0x5c
. 그것이 나오는 것에 따라, 기본적으로 MySQL의 5.6에서 지원 (5)와 같은 인코딩이있다 : big5
, cp932
, gb2312
, gbk
와 sjis
. gbk
여기서 선택 하겠습니다.
자, SET NAMES
여기서 의 사용에 주목하는 것이 매우 중요합니다 . 이것은 서버에 문자 세트를 설정합니다 . 또 다른 방법이 있지만, 우리는 곧 도착할 것입니다.
페이로드
이 주입에 사용할 페이로드는 바이트 시퀀스로 시작합니다 0xbf27
. 에서 gbk
유효하지 않은 멀티 바이트 문자입니다. 에서 latin1
문자열 ¿'
입니다. 에 그주의 latin1
하고 gbk
, 0x27
자신이 리터럴에 '
문자.
우리는이 페이로드를 선택했다. 왜냐하면 호출 하면 문자 앞에 addslashes()
ASCII \
즉을 삽입 0x5c
하기 때문 '
이다. 우리가 바람 것 그래서 0xbf5c27
, 어떤에서이 gbk
두 개의 문자 순서입니다 : 0xbf5c
다음에 0x27
. 즉, 유효한 문자 뒤에 이스케이프 처리되지 않은이 '
있습니다. 그러나 우리는을 사용하지 않습니다 addslashes()
. 다음 단계로 넘어갑니다 ...
$ stmt-> execute ()
여기서 중요한 사실은 PDO가 기본적으로 실제 준비된 명령문을 수행 하지 않는다는 것입니다. MySQL을 위해 에뮬레이션합니다. 따라서 PDO는 내부적 mysql_real_escape_string()
으로 각 바인딩 문자열 값에서 (MySQL C API 함수)를 호출하여 쿼리 문자열을 작성합니다 .
C API 호출 은 연결 문자 집합을 알고 있다는 mysql_real_escape_string()
점과 다릅니다 addslashes()
. 따라서 서버가 기대하는 문자 세트에 대해 이스케이프를 올바르게 수행 할 수 있습니다. 그러나 지금까지 클라이언트 latin1
는 연결에 대해 계속 사용하고 있다고 생각합니다 . 우리는 서버에 사용하고 있다고 말 gbk
했지만 클라이언트는 여전히 서버 를 사용 한다고 생각합니다 latin1
.
따라서 mysql_real_escape_string()
백 슬래시 를 삽입 하라는 호출은 '
"탈출 된"컨텐츠에 자유롭게 걸려있는 문자를 갖습니다 ! 우리가보고 있다면 사실, $var
에서 gbk
문자 집합, 우리는 볼 것 :
縗 'OR 1 = 1 / *
정확히 공격이 요구하는 것입니다.
쿼리
이 부분은 형식 일 뿐이지 만 렌더링 된 쿼리는 다음과 같습니다.
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
축하합니다. PDO Prepared Statements를 사용하여 프로그램을 성공적으로 공격했습니다.
간단한 수정
이제 에뮬레이트 된 준비된 문장을 비활성화하여 이것을 막을 수 있다는 점에 주목할 가치가 있습니다.
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
이로 인해 일반적으로 준비된 명령문이 생성됩니다 (예 : 데이터가 쿼리와 별도의 패킷으로 전송 됨). 그러나 PDO는 MySQL이 기본적으로 준비 할 수없는 명령문 ( 매뉴얼에 나열 될 수 있지만 적절한 서버 버전을 선택하도록주의 해야하는 명령문)을 에뮬레이션하는 것으로 자동으로 대체 됩니다 .
올바른 수정
여기서 문제는 C mysql_set_charset()
대신 C API를 호출하지 않았다는 것입니다 SET NAMES
. 만약 그렇다면, 2006 년 이후로 MySQL 릴리스를 사용한다면 괜찮을 것입니다.
당신은 이전 MySQL의 릴리스, 다음 사용하는 경우 버그 에서 mysql_real_escape_string()
같은 우리의 페이로드에있는 것과 같은 잘못된 멀티 바이트 문자를 목적으로 탈출을위한 단일 바이트로 처리 된 것을 의미 클라이언트가 올바르게 연결 인코딩을 통보했다하더라도 그래서이 공격 것 등을 여전히 성공합니다. 버그는 MySQL 4.1.20 , 5.0.22 및 5.1.11 에서 수정되었습니다 .
그러나 최악의 부분은 5.3.6까지 PDO
C API를 공개하지 않았기 mysql_set_charset()
때문에 이전 버전에서는 가능한 모든 명령에 대해이 공격을 막을 수는 없습니다 ! 이제 DSN 매개 변수 로 노출되며 대신에 사용해야합니다 SET NAMES
...
구원의 은총
처음에 말했듯이이 공격이 작동하려면 데이터베이스 연결이 취약한 문자 세트를 사용하여 인코딩되어야합니다. utf8mb4
이다 취약하지 지원할 수 아직하고 모든 당신의 MySQL 5.5.3 이후 대신-하지만 그것은 단지되었습니다 사용할 수를 사용하여 선택할 수 있도록 유니 코드 문자를. 대안은 utf8
또한, 이는 취약하지 및 유니 코드의 전체 지원할 수있는 기본 다국어 평면 .
또는 NO_BACKSLASH_ESCAPES
SQL 모드를 활성화 할 수 있습니다.이 모드는 다른 작업 중에서의 작업을 변경합니다 mysql_real_escape_string()
. 이 모드가 활성화 된 상태 0x27
로 대체됩니다 0x2727
보다는 0x5c27
탈출 과정이 이렇게하고 할 수없는 그들이 이전에 존재하지 않았던 취약한 인코딩의 유효 문자를 생성 (즉 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는 항상 진실 된 준비된 진술을하기 때문입니다.
마무리
만약 너라면:
- 최신 버전의 MySQL (최근 5.1, 모든 5.5, 5.6 등) 및 PDO의 DSN 문자 세트 매개 변수 (PHP ≥ 5.3.6)를 사용하십시오.
또는
- 연결 인코딩에 취약한 문자 세트를 사용하지 마십시오 (
utf8
/ latin1
/ ascii
/ 등 만 사용 )
또는
- 사용
NO_BACKSLASH_ESCAPES
SQL 모드를
당신은 100 % 안전합니다.
그렇지 않으면 PDO Prepared Statements를 사용하더라도 취약합니다 ...
추가
필자는 기본 버전을 변경하여 향후 버전의 PHP를 준비하지 않도록 패치를 천천히 연구하고 있습니다. 내가 겪고있는 문제는 그렇게 할 때 많은 테스트가 중단된다는 것입니다. 한 가지 문제는 에뮬레이트 된 준비가 실행시 구문 오류 만 발생하지만, 진정한 준비는 준비시 오류가 발생한다는 것입니다. 따라서 문제가 발생할 수 있습니다 (테스트가 중단되는 이유의 일부 임).