문제

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

$dbh = new PDO("blahblah");

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

PDO 문서에는 다음과 같이 나와 있습니다.

준비된 문의 매개변수는 인용할 필요가 없습니다.운전기사가 대신 처리해 드립니다.

SQL 주입을 피하기 위해 이것이 정말로 내가 해야 할 전부입니까?정말 그렇게 쉬운가요?

차이가 있다면 MySQL을 가정할 수 있습니다.또한 SQL 주입에 대해 준비된 명령문을 사용하는 것에 대해서만 궁금합니다.이러한 맥락에서 나는 XSS나 기타 가능한 취약점에 관심이 없습니다.

도움이 되었습니까?

해결책

짧은 대답은 아니요, 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));

특정 상황에서는 1개 이상의 행이 반환됩니다.여기서 무슨 일이 일어나고 있는지 분석해 보겠습니다.

  1. 문자 세트 선택

    $pdo->query('SET NAMES gbk');
    

    이 공격이 작동하려면 서버가 인코딩할 연결에서 기대하는 인코딩이 필요합니다. ' ASCII에서와 같이, 즉 0x27 그리고 마지막 바이트가 ASCII인 문자를 가지려면 \ 즉. 0x5c.결과적으로 MySQL 5.6에서는 기본적으로 5가지 인코딩이 지원됩니다. big5, cp932, gb2312, gbk 그리고 sjis.우리는 선택할 것입니다 gbk 여기.

    이제 사용 방법에 주목하는 것이 매우 중요합니다. SET NAMES 여기.이것은 문자 세트를 설정합니다 서버에서.다른 방법도 있지만 곧 그 방법에 도달하겠습니다.

  2. 페이로드

    이 주입에 사용할 페이로드는 바이트 시퀀스로 시작합니다. 0xbf27.~ 안에 gbk, 이는 잘못된 멀티바이트 문자입니다.~에 latin1, 그것은 문자열입니다 ¿'.참고하세요 latin1 그리고 gbk, 0x27 그 자체는 문자 그대로이다 ' 성격.

    우리가 이 페이로드를 선택한 이유는 addslashes() 그 위에 ASCII를 삽입합니다. \ 즉. 0x5c, 이전에 ' 성격.그래서 우리는 결국 0xbf5c27, 어느 gbk 두 문자 시퀀스입니다. 0xbf5c 이어서 0x27.또는 다른 말로 하면, 유효한 문자 뒤에 이스케이프되지 않은 문자가 옵니다. '.하지만 우리는 사용하지 않습니다 addslashes().그럼 다음 단계로...

  3. $stmt->실행()

    여기서 알아야 할 중요한 점은 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 /*

    이것이 바로 공격에 필요한 것입니다.

  4. 쿼리

    이 부분은 형식적인 부분이지만 렌더링된 쿼리는 다음과 같습니다.

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

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

간단한 수정

이제 에뮬레이트된 준비된 문을 비활성화하여 이를 방지할 수 있다는 점은 주목할 가치가 있습니다.

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

이것은 대개 실제로 준비된 진술이 생성됩니다(예:데이터는 쿼리와 별도의 패킷으로 전송됩니다.그러나 PDO는 자동으로 대체 MySQL이 기본적으로 준비할 수 없는 명령문을 에뮬레이트하는 방법:그럴 수 있는 것들은 나열됨 설명서에 나와 있지만 적절한 서버 버전을 선택하도록 주의하세요).

올바른 수정

여기서 문제는 우리가 C API를 호출하지 않았다는 것입니다. mysql_set_charset() 대신에 SET NAMES.그렇다면 2006년 이후 MySQL 릴리스를 사용하고 있다면 괜찮을 것입니다.

이전 MySQL 릴리스를 사용하는 경우 벌레 ~에 mysql_real_escape_string() 이는 페이로드에 있는 것과 같은 유효하지 않은 멀티바이트 문자가 이스케이프 목적으로 단일 바이트로 처리되었음을 의미합니다. 클라이언트가 연결 인코딩에 대해 올바르게 알고 있었더라도 따라서 이 공격은 여전히 ​​성공할 것입니다.MySQL에서 버그가 수정되었습니다. 4.1.20, 5.0.22 그리고 5.1.11.

하지만 가장 나쁜 점은 PDO C API를 공개하지 않았습니다. mysql_set_charset() 5.3.6까지이므로 이전 버전에서는 할 수 없다 가능한 모든 명령에 대해 이 공격을 방지하십시오!이제 다음과 같이 노출됩니다. DSN 매개변수, 사용해야 합니다. 대신에 SET NAMES...

구원의 은혜

처음에 말했듯이 이 공격이 작동하려면 데이터베이스 연결이 취약한 문자 집합을 사용하여 인코딩되어야 합니다. utf8mb4 ~이다 취약하지 않음 그래도 지원할 수 있습니다 모든 유니코드 문자:따라서 이를 대신 사용하도록 선택할 수 있습니다. 그러나 이는 MySQL 5.5.3 이후에만 사용할 수 있습니다.대안은 utf8, 이는 또한 취약하지 않음 유니코드 전체를 지원할 수 있습니다. 기본 다국어 비행기.

또는 다음을 활성화할 수 있습니다. NO_BACKSLASH_ESCAPES (다른 무엇보다도) 작업을 변경하는 SQL 모드 mysql_real_escape_string().이 모드를 활성화하면, 0x27 로 대체됩니다 0x2727 오히려 0x5c27 따라서 탈출 과정 할 수 없다 이전에 존재하지 않았던 취약한 인코딩에서 유효한 문자를 생성합니다(예: 0xbf27 아직 0xbf27 등) - 따라서 서버는 여전히 문자열을 유효하지 않은 것으로 거부합니다.그러나 참조 @eggyal의 답변 이 SQL 모드를 사용하여 발생할 수 있는 다른 취약점에 대해(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 준비된 문을 사용하고 있더라도 ...

부록

저는 PHP의 향후 버전을 준비하기 위해 에뮬레이트하지 않도록 기본값을 변경하는 패치 작업을 천천히 진행해 왔습니다.제가 겪고 있는 문제는 그렇게 할 때 많은 테스트가 중단된다는 것입니다.한 가지 문제는 에뮬레이트된 준비는 실행 시에만 구문 오류를 발생시키지만 실제 준비는 준비 시 오류를 발생시킨다는 것입니다.따라서 문제가 발생할 수 있습니다(그리고 테스트가 지루해지는 이유 중 하나입니다).

다른 팁

준비된 문/매개변수화된 쿼리는 일반적으로 다음을 방지하는 데 충분합니다. 1차 주문 그 진술에 주입*.애플리케이션의 다른 곳에서 확인되지 않은 동적 SQL을 사용하는 경우에도 여전히 취약합니다. 2차 주입.

2차 주입은 데이터가 쿼리에 포함되기 전에 데이터베이스를 통해 한 번 순환되었으며 추출하기가 훨씬 어렵다는 것을 의미합니다.AFAIK, 공격자가 사회 공학적으로 진입하는 것이 일반적으로 더 쉽기 때문에 실제 엔지니어링된 2차 공격을 거의 볼 수 없지만 때로는 추가 양성으로 인해 2차 버그가 발생하는 경우도 있습니다. ' 문자 또는 이와 유사한 것.

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

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

사용자 이름에 다른 제한 사항이 없는 경우에도 준비된 문은 삽입 시 위의 포함된 쿼리가 실행되지 않는지 확인하고 값을 데이터베이스에 올바르게 저장합니다.그러나 나중에 애플리케이션이 데이터베이스에서 사용자 이름을 검색하고 문자열 연결을 사용하여 해당 값을 새 쿼리에 포함한다고 가정해 보십시오.다른 사람의 비밀번호를 볼 수도 있습니다.사용자 테이블의 처음 몇 이름은 관리자인 경향이 있으므로 방금 팜을 제공했을 수도 있습니다.(참고:이것이 비밀번호를 일반 텍스트로 저장하지 않는 또 하나의 이유입니다!)

그러면 준비된 명령문은 단일 쿼리에 충분하지만 그 자체로는 충분하다는 것을 알 수 있습니다. ~ 아니다 애플리케이션 내의 데이터베이스에 대한 모든 액세스가 안전한 코드를 사용하도록 강제하는 메커니즘이 부족하기 때문에 전체 애플리케이션에서 SQL 주입 공격으로부터 보호하기에 충분합니다.그러나 좋은 애플리케이션 설계의 일부로 사용됩니다. 여기에는 코드 검토, 정적 분석, ORM 사용, 데이터 계층 또는 동적 SQL을 제한하는 서비스 계층 등이 포함될 수 있습니다. 준비된 진술 ~이다 SQL 주입 문제를 해결하기 위한 기본 도구입니다. 데이터 액세스가 프로그램의 나머지 부분과 분리되는 등 좋은 애플리케이션 설계 원칙을 따르면 모든 쿼리가 매개 변수화를 올바르게 사용하는지 쉽게 적용하거나 감사할 수 있습니다.이 경우 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']) );

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

$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으로 내장)로 JS를 비활성화하거나 패턴을 편집 할 수 있습니다.

따라서 SQL 주입을 방지하기 위해 컨트롤러 내부의 입력 날짜 백엔드를 소독 할 수 있습니다.

get 및 입력 값을 소독하기 위해 Filter_Input () 기본 PHP 함수를 사용하도록 제안하고 싶습니다.

현명한 데이터베이스 쿼리를 위해 보안을 진행하려면 정규식을 사용하여 데이터 형식을 검증하도록 제안합니다. 이 경우 preg_match ()가 도움이 될 것입니다! 그러나 조심하세요! REGEX 엔진은 그렇게 가볍지 않습니다. 필요한 경우에만 사용하십시오. 그렇지 않으면 응용 프로그램 성능이 줄어 듭니다.

보안에는 비용이 있지만 성과를 낭비하지 마십시오!

Easy example:

Get에서받은 값이 숫자인지를 두 번 확인하려면, 99 If (! preg_match ( '/[0-9] {1,2}/')) {...}는

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

따라서 최종 대답은 다음과 같습니다. "아니오! PDO 준비된 진술은 모든 종류의 SQL 주입을 방지하지는 않습니다"; 예상치 못한 값을 방지하지 않고 예상치 못한 연결 만하면됩니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top