다음과 같은 코드가 있다고 가정 해 봅시다.
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
PDO 문서는 다음과 같이 말합니다.
준비된 명령문에 대한 매개 변수는 인용 할 필요가 없습니다. 운전자가 대신 처리합니다.
이것이 SQL 주입을 피하기 위해 정말로해야하는 전부입니까? 정말 쉬운가요?
차이가 나는 경우 MySQL을 가정 할 수 있습니다. 또한 SQL 주입에 대해 준비된 명령문을 사용하는 것에 대해 정말로 궁금합니다. 이와 관련하여 XSS 나 다른 가능한 취약점에 대해서는 신경 쓰지 않습니다.
답변
짧은 대답은 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를 준비하지 않도록 패치를 천천히 연구하고 있습니다. 내가 겪고있는 문제는 그렇게 할 때 많은 테스트가 중단된다는 것입니다. 한 가지 문제는 에뮬레이트 된 준비가 실행시 구문 오류 만 발생하지만, 진정한 준비는 준비시 오류가 발생한다는 것입니다. 따라서 문제가 발생할 수 있습니다 (테스트가 중단되는 이유의 일부 임).
답변
준비된 명령문 / 매개 변수화 된 쿼리는 일반적으로 해당 명령문에 1 차 주입 을 방지 하기에 충분 합니다 * . 응용 프로그램의 다른 곳에서 확인되지 않은 동적 SQL을 사용하는 경우 여전히 2 차 주입에 취약합니다 .
2 차 주입은 쿼리에 포함되기 전에 데이터베이스를 통해 데이터가 한 번 순환되어 해제하기가 훨씬 어렵다는 것을 의미합니다. AFAIK, 공격자가 소셜 엔지니어링을 쉽게 수행 할 수 있기 때문에 실제 엔지니어링 된 2 차 공격은 거의 볼 수 없지만 때로는 2 차적인 버그로 인해 좀 더 양성적인 '
캐릭터가 생길 수 있습니다.
나중에 쿼리에서 리터럴로 사용되는 데이터베이스에 값을 저장할 수있는 경우 2 차 주입 공격을 수행 할 수 있습니다. 예를 들어, 웹 사이트에서 계정을 만들 때 다음과 같은 정보를 새 사용자 이름으로 입력한다고 가정합니다 (이 질문에 대한 MySQL DB 가정).
' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
사용자 이름에 다른 제한이없는 경우 준비된 명령문은 삽입시 위의 내장 쿼리가 실행되지 않도록하고 데이터베이스에 값을 올바르게 저장합니다. 그러나 나중에 응용 프로그램이 데이터베이스에서 사용자 이름을 검색하고 문자열 연결을 사용하여 해당 값을 새 쿼리에 포함한다고 가정합니다. 다른 사람의 비밀번호가 표시 될 수 있습니다. users 테이블의 처음 몇 개의 이름은 관리자 인 경향이 있으므로 팜을 제공했을 수도 있습니다. (또한 참고 : 이것은 암호를 일반 텍스트로 저장하지 않는 또 하나의 이유입니다!)
그러므로 준비된 명령문은 단일 쿼리에 충분하지만, 그 자체로는 전체 애플리케이션 전체에서 SQL 주입 공격으로부터 보호하기에 충분 하지 않습니다 . 이는 애플리케이션 내에서 데이터베이스에 대한 모든 액세스를 안전하게 사용하는 메커니즘이 없기 때문입니다. 암호. 그러나 코드 검토 또는 정적 분석 또는 ORM, 데이터 계층 또는 동적 SQL을 제한하는 서비스 계층 사용과 같은 사례를 포함 할 수있는 우수한 응용 프로그램 설계의 일부로 사용되는 준비된 문 은 Sql Injection을 해결하기위한 기본 도구입니다. 문제.데이터 액세스가 프로그램의 나머지 부분과 분리되도록 올바른 응용 프로그램 디자인 원칙을 따르는 경우 모든 쿼리가 매개 변수화를 올바르게 사용하도록 쉽게 적용하거나 감사 할 수 있습니다. 이 경우 SQL 주입 (1 차 및 2 차)이 완전히 방지됩니다.
* 그것은 MySQL의 / PHP는 (좋아이었다) 넓은 문자가 포함 된 경우 매개 변수 처리에 대한 단지 벙어리 것으로 나타났다 및 정지가 드문 에 명시된 경우 여기에 답을 높은 투표 다른 그 주입이 매개 변수를 통해 어긋날 수는 질문.
답변
아니요, 항상 그런 것은 아닙니다.
사용자 입력을 쿼리 자체에 배치 할 수 있는지 여부에 따라 다릅니다. 예를 들면 다음과 같습니다.
$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']) );
참고 : PDL을 사용하여 DDL (Data Definition Language) 외부로 이동하는 데이터를 바인딩 할 수 없습니다. 즉, 작동하지 않습니다.
$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');
때문에 위의 작업은하지 않는 이유 DESC
와 ASC
하지 않은 데이터 . PDO는 데이터에 대해서만 이스케이프 할 수 있습니다 . 둘째, 당신은 '
그 주위 에 따옴표를 넣을 수 없습니다 . 사용자가 선택한 정렬을 허용하는 유일한 방법은 수동으로 필터링하여 DESC
또는 중 하나 인지 확인하는 것 ASC
입니다.
답변
예, 충분합니다. 주입 유형 공격의 작동 방식은 어떤 식 으로든 인터프리터 (데이터베이스)가 코드 인 것처럼 데이터 여야하는 무언가를 평가하도록하는 것입니다. 이것은 동일한 매체에서 코드와 데이터를 혼합 한 경우에만 가능합니다 (예 : 쿼리를 문자열로 구성 할 때).
매개 변수가있는 쿼리는 코드와 데이터를 별도로 보내서 작동하므로 절대로 구멍을 찾을 수 없습니다 .
그래도 다른 주입 유형 공격에 취약 할 수 있습니다. 예를 들어, HTML 페이지에서 데이터를 사용하는 경우 XSS 유형 공격을받을 수 있습니다.
답변
이것은 충분하지 않습니다 (특정 경우)! 기본적으로 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()
를 들어 올바른 인코딩 및 인용 스타일로 다시 사용 하십시오.
답변
개인적으로 나는 사용자 입력을 신뢰할 수 없으므로 항상 데이터에 대해 항상 위생을 실행합니다. 그러나 자리 표시 자 / 매개 변수 바인딩을 사용하면 입력 된 데이터가 sql 문으로 서버로 전송 된 다음 함께 바인딩됩니다. 여기서 핵심은 제공된 데이터를 특정 유형 및 특정 용도에 바인딩하고 SQL 문의 논리를 변경할 수있는 기회를 없애는 것입니다.
답변
html 또는 js 검사를 사용하여 SQL 주입 프런트 엔드를 방지하려는 경우에도 프런트 엔드 검사는 “우회”되어야합니다.
현재 Firefox 또는 Chrome에 내장 된 프론트 엔드 개발 도구를 사용하여 j를 비활성화하거나 패턴을 편집 할 수 있습니다.
따라서 SQL 삽입을 방지하려면 컨트롤러 내부에서 입력 날짜 백엔드를 삭제하는 것이 좋습니다.
GET 및 INPUT 값을 삭제하기 위해 filter_input () 기본 PHP 함수를 사용하도록 제안하고 싶습니다.
현명한 데이터베이스 쿼리에 대해 보안을 유지하려면 정규식을 사용하여 데이터 형식의 유효성을 검사하는 것이 좋습니다. 이 경우 preg_match ()가 도움이 될 것입니다! 하지만 조심해! 정규식 엔진이 너무 가볍지 않습니다. 필요한 경우에만 사용하십시오. 그렇지 않으면 응용 프로그램 성능이 저하됩니다.
보안에는 비용이 들지만 성능을 낭비하지 마십시오!
쉬운 예 :
GET에서 수신 한 값이 숫자인지 다시 확인하려면 99보다 작은 경우 99보다 작습니다 (! preg_match ( ‘/ [0-9] {1,2} /’)) {…}
if (isset($value) && intval($value)) <99) {...}
따라서 최종 답변은 “아니요! PDO 준비된 문장이 모든 종류의 SQL 삽입을 막지는 못합니다”; 예기치 않은 값을 막지 않고 예기치 않은 연결을 막습니다.