목차
CDATA 키워드
XML에서의 비교연산시 사용하므로 xml 문서 내 쿼리 안에 ’<>’, ’&’ 등의 특수문자가 포함될 경우 에러를 방지한다.
<![CDATA[
score >= 10 AND score < 100
]]>
정확히는 CDATA는 SQL 키워드가 아니라 XML의 구문이다. XML 파서가 CDATA 내부의 <, >, &를 태그나 엔티티의 시작으로 해석하지 않도록 한다. 쿼리의 실행 방식이나 SQL 인젝션 방어 방식은 바꾸지 않는다.
예를 들어 XML 기반 매퍼에 비교 조건을 적을 때 아래처럼 쓸 수 있다.
<select id="findRecent" resultType="Record">
SELECT * FROM records
WHERE score <![CDATA[ >= ]]> #{minimumScore}
</select>
CDATA 대신 >=처럼 XML 엔티티로 이스케이프해도 된다. 값은 CDATA에 문자열로 이어 붙이지 말고 위 예시처럼 바인딩한다. CDATA 안에는 종료 표시인 ]]>를 그대로 넣을 수 없다.
CDATA 범위를 작게 잡는 이유
XML 기반 SQL 매퍼에서 동적 태그까지 CDATA로 감싸면 XML 파서는 그 태그를 구조가 아니라 평범한 글자로 본다. 비교 연산자 주변만 감싸고 나머지 매퍼 태그는 XML 구조로 남겨 둔다.
<!-- 비교 연산자만 CDATA로 처리 -->
<if test="minimumScore != null">
AND score <![CDATA[ >= ]]> #{minimumScore}
</if>
단순한 비교에는 AND score >= #{minimumScore}라고 써도 된다. XML 파일 안의 텍스트가 유효한지와 SQL이 올바른지는 별개의 문제다. XML 파싱이 통과해도 테이블명, 컬럼명, 매개변수 바인딩, 데이터베이스 SQL 문법은 따로 확인해야 한다. CDATA는 사용자 입력을 이스케이프하는 수단이 아니므로 SQL 값을 직접 이어 붙이는 방식과 혼동하지 않는다.
파싱 오류를 재현하고 수정하기
XML의 요소 내용에 <를 그대로 넣으면 XML 파서는 이를 새 태그의 시작으로 해석한다. 아래 첫 매퍼는 score < 100 부분 때문에 XML 단계에서 실패한다. 데이터베이스가 SQL을 실행하기도 전이다.
<!-- 잘못된 XML: '<'가 태그의 시작으로 해석됨 -->
<select id="findScores" resultType="int">
SELECT score FROM exam WHERE score < 100
</select>
<!-- 방법 1: XML 엔티티 -->
<select id="findScores" resultType="int">
SELECT score FROM exam WHERE score < #{maxScore}
</select>
<!-- 방법 2: 비교 연산자만 CDATA -->
<select id="findScores" resultType="int">
SELECT score FROM exam WHERE score <![CDATA[ < ]]> #{maxScore}
</select>
두 번째와 세 번째는 XML 파싱 후 SQL의 score < ?에 해당하는 조건을 만든다. 매개변수 maxScore는 MyBatis의 #{...}로 바인딩한다. ${...}로 사용자 값을 SQL 텍스트에 직접 끼워 넣으면 CDATA를 썼어도 SQL 인젝션 위험이 생긴다. CDATA가 처리하는 것은 XML 예약 문자이고 SQL 값의 안전한 전달은 바인딩이 담당한다.
검증은 두 단계로 나눈다. 먼저 매퍼 XML을 애플리케이션에서 로드해 XML 파싱 오류가 사라졌는지 본다. 다음으로 99, 100 같은 경계 데이터를 넣어 99는 선택되고 100은 제외되는지 확인한다. XML이 유효해도 <=와 <를 혼동하면 업무 결과는 틀리다. ]]>를 포함하는 텍스트를 CDATA 하나에 그대로 넣을 수 없다는 XML 규칙도 기억한다.
참고: W3C XML CDATA, MyBatis SQL 매퍼.