
SQL 인젝션은 20년 넘게 알려진 취약점이지만 지금도 점검에서 가장 자주 나옵니다. 이유는 간단합니다 — 문자열을 이어 붙여 쿼리를 만드는 코드가 '잘 돌아가기' 때문입니다. PHP 에서 입력값을 다루는 기본 원칙을 받기 → 검증 → 쓰기 → 내보내기 네 단계로 정리합니다.

1. 준비된 문장(Prepared Statement) — 예외 없이
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('SELECT id, title FROM posts WHERE user_id = :uid AND status = :st');
$stmt->execute([':uid' => $userId, ':st' => 'published']);
값은 쿼리 문자열에 들어가지 않고 따로 전달되므로 입력에 따옴표가 있어도 구조가 바뀌지 않습니다. ATTR_EMULATE_PREPARES => false 로 두면 DB 가 직접 준비된 문장을 처리합니다. 테이블명·컬럼명·정렬 방향처럼 바인딩할 수 없는 부분은 허용 목록에서만 고릅니다.
$allowed = ['created_at', 'title'];
$order = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'created_at';
2. 입력 검증 — 형식에 맞지 않으면 거부
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 10000]]);
if ($page === false || $page === null) { http_response_code(400); exit; }
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
검증은 '나쁜 문자를 지우는 것' 이 아니라 '기대한 형식인지 확인하고 아니면 거부하는 것' 입니다. 지우는 방식(블랙리스트)은 늘 빠지는 경우가 생깁니다.

3. 출력 이스케이프 — XSS 는 내보낼 때 막는다
echo htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
HTML 본문, 속성, 자바스크립트, URL 은 각각 이스케이프 방법이 다릅니다. 템플릿 엔진(Twig 등)의 자동 이스케이프를 쓰면 대부분 해결되고, 직접 echo 할 때는 위 함수를 습관으로 둡니다. DB 에 넣을 때 이스케이프하고 꺼낼 때 그대로 쓰는 방식은 문맥이 바뀌면 깨지니 내보내는 시점에 합니다.
4. 파일 업로드
사용자가 보낸 파일명은 믿지 않습니다. 확장자와 MIME 을 허용 목록으로 검사하고, 서버가 만든 임의 파일명으로 웹루트 밖에 저장하고, 실행 권한을 주지 않습니다. 이미지라면 다시 인코딩해 저장하는 것도 좋은 방법입니다.
5. 오류는 사용자에게 보이지 않게
운영에서는 display_errors=Off, 로그로만 남깁니다. 쿼리·경로가 담긴 오류 화면은 공격자에게 지도가 됩니다.
규칙은 하나로 요약됩니다. SQL 은 준비된 문장, 입력은 검증, 출력은 이스케이프. 이 셋을 예외 없이 지키면 웹 취약점의 큰 부분이 사라집니다.

댓글