PHP PDO Prepared Statement SQL 인젝션 방어 방법: 바인딩 문법과 필수 보안 옵션 가이드

SQL 인젝션 취약점 발생 원인과 PDO Prepared Statement의 방어 원리

PHP 웹 애플리케이션에서 SQL 인젝션(SQL Injection)은 사용자 입력값을 SQL 쿼리 문자열에 단순 결합할 때 발생합니다. 공격자가 작은따옴표나 세미콜론 같은 SQL 제어 문자를 입력값에 포함하면, 데이터베이스 엔진은 이를 데이터가 아닌 SQL 문법 구조의 일부로 해석하여 비정상적인 권한 탈취나 데이터 유출을 실행하게 됩니다.

PHP Data Objects(PDO)의 Prepared Statement는 이러한 공격을 원천적으로 차단하는 가장 효과적인 수단입니다. 이 기술은 쿼리의 실행 흐름을 ‘SQL 문법 구문 분석(Prepare)’ 단계와 ‘실제 데이터 전달(Execute)’ 단계로 완전히 분리합니다.

데이터베이스 엔진은 개발자가 전달한 쿼리 템플릿의 문법 구조를 먼저 분석하고 컴파일하여 실행 계획을 수립합니다. 이후 외부에서 유입된 파라미터 값은 이미 파싱이 끝난 플레이스홀더 위치에 순수한 리터럴 데이터로만 대입되므로, 공격자가 어떠한 SQL 제어 문자를 입력하더라도 쿼리 문법 자체를 변조하는 것이 구조적으로 불가능해집니다.





PDO Prepared Statement 기본 문법: bindParam과 bindValue의 동작 차이

PDO에서 Prepared Statement를 작성할 때는 물음표(?)를 사용하는 위치 기반 플레이스홀더나 콜론(:name)으로 시작하는 명명된 플레이스홀더를 선언한 뒤 값을 바인딩합니다. 이때 bindParam과 bindValue의 동작 차이를 정확히 인지해야 의도치 않은 런타임 버그와 데이터 오염을 예방할 수 있습니다.

PDO Prepared Statement 기본 문법: bindParam과 bindValue의 동작 차이 설명 이미지

1. bindParam: 변수의 참조(Reference) 전달

bindParam 메서드는 변수의 메모리 주소를 참조 형태로 바인딩합니다. 따라서 바인딩 시점의 값이 즉시 쿼리에 고정되는 것이 아니라, execute() 메서드가 호출되는 바로 그 순간의 변수 값이 평가되어 쿼리에 전달됩니다.

  • 적용 환경: 동일한 Statement 객체를 유지한 채 루프 내부에서 변수 값만 바꿔 여러 번 execute()를 실행할 때 메모리와 성능 면에서 적합합니다.
  • 주의 사항: 리터럴 상수나 함수 반환값을 직접 전달할 수 없으며 오직 변수만 인자로 전달할 수 있습니다.

2. bindValue: 값(Value)의 즉시 복사 전달

bindValue 메서드는 호출 시점에 전달된 값을 즉시 평가하여 해당 플레이스홀더에 복사합니다. 이후 코드에서 바인딩했던 변수의 값을 변경하더라도 이미 바인딩된 쿼리 값에는 아무런 영향을 주지 않습니다.

  • 적용 환경: 단일 행 조회나 단순 조건문 쿼리처럼 특정 시점의 고정된 값이나 상수, 암호화 함수 결과를 안전하게 주입할 때 직관적이고 안전합니다.
  • 주의 사항: 반복 루프에서 동일한 파라미터명을 재사용할 경우 매 루프마다 명시적으로 bindValue를 다시 호출해야 합니다.

필수 보안 옵션: ATTR_EMULATE_PREPARES false 설정과 연결 구성

PDO 객체를 생성하고 Prepared Statement를 작성했더라도, 기본 드라이버 설정을 방치하면 기대했던 완벽한 보안 수준을 확보하지 못할 수 있습니다. PHP PDO의 MySQL 드라이버는 성능 최적화와 구형 서버 호환성을 이유로 ATTR_EMULATE_PREPARES 옵션이 기본적으로 활성화(true)되어 있는 경우가 많습니다.

필수 보안 옵션: ATTR_EMULATE_PREPARES false 설정과 연결 구성 설명 이미지

에뮬레이션 모드가 켜져 있으면 PHP 내부에서 문자열 치환과 자체 이스케이프 처리를 거쳐 데이터베이스로 단일 완성형 쿼리를 전송합니다. 이는 진정한 데이터베이스 엔진 레벨의 Prepared Statement가 아니므로, 데이터베이스 연결 문자셋(예: GBK, Big5 등 다중 바이트 문자셋)과 PHP 내부 인코딩 처리가 어긋날 경우 특수 인코딩 우회 공격에 노출될 수 있습니다.

  1. 에뮬레이션 비활성화: PDO 인스턴스 생성 시 PDO::ATTR_EMULATE_PREPARES => false 속성을 반드시 지정하여 데이터베이스 서버 네이티브 프리페어를 강제합니다.
  2. 엄격한 오류 모드 지정: PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION을 적용하여 바인딩 오류나 실행 실패 시 예외를 던지도록 구성합니다.
  3. 안전한 문자셋 설정: DSN(Data Source Name) 연결 문자열에 charset=utf8mb4를 명시하여 연결 계층의 인코딩 불일치를 예방합니다.

Prepared Statement로 바인딩할 수 없는 동적 식별자 안전 처리법

Prepared Statement는 테이블명, 컬럼명, 또는 ORDER BY 정렬 방향(ASC/DESC)과 같은 SQL 식별자 및 문법 키워드 위치에는 바인딩할 수 없습니다. 데이터베이스 엔진이 쿼리 구조를 미리 파싱하려면 어떤 테이블과 어떤 컬럼을 조회할지 문법적으로 확정되어 있어야 하기 때문입니다.

사용자의 선택에 따라 정렬 기준이나 조회 대상 테이블이 동적으로 변해야 한다면 파라미터 바인딩 대신 다음과 같은 화이트리스트(Whitelist) 검증 방식을 반드시 거쳐야 합니다.

  • 허용 목록 배열 정의: 애플리케이션 내부에서 사용 가능한 테이블명이나 컬럼명, 정렬 방향을 PHP 배열로 명시적으로 하드코딩합니다.
  • 엄격한 일치성 검증: in_array($input, $allowedList, true)와 같이 세 번째 인자로 엄격한 타입 비교(strict)를 적용하여 허용 목록에 완벽히 일치하는 값만 통과시킵니다.
  • 예외 처리 및 기본값 대체: 검증을 통과하지 못한 비정상적인 입력값이 들어오면 사전에 정의한 기본값(예: ‘created_at’, ‘DESC’)으로 강제 대체하거나 유효하지 않은 요청 예외를 발생시킵니다.
WordPress Appliance - Powered by TurnKey Linux