mybatis의 xml 파일에서 '${id}' 이런 식으로 쿼리문을 작성했을 때, 단순히 append가 된다는 것은 저번에 이야기를 해 드렸습니다. 어떻게 이런 일이 일어나는지 조금 깊게 들어가 보도록 하겠습니다.

 

 

 일단 id는 'or 1 = 1-- 이고, pw는 1234입니다. 이렇게 입력할 겁니다.

 

 

 실험으로 쓰일 sql 문은 다음과 같이 xml 파일에 작성하였습니다.

 


 일단, 제 Usert 클래스입니다. id, pw, num 필드가 있다는 것을 알 수 있습니다.

 

 

 다음에, SqlSource를 implements한 DynamicSqlSource 클래스가 있는데요. 이 안에 getBoundSql이 있습니다. 느낌상 이 메서드가 바운드를 시킬 거 같으니, 적당한 위치에 브레이크 포인트를 걸어보겠습니다.

 

 

 이 때 parameterObject는 id가 ' or 1 = 1-- 이고, pw가 1234이고, num이 0인 객체입니다. context를 만들기 위해서, 인자로 넘어온 parameterObject가 필요합니다. 그리고 그것을 그 밑에 줄의 rootSqlNode.apply(context);가 쓰니, 이 구문으로 들어가 보도록 하겠습니다.

 

 

 MixedSqlNode로 들어왔습니다. 여기서 뭔지는 잘 모르겠지만, sqlNode.apply(context)로 또 들어가 보겠습니다.

 

 

 그러면, TextSqlNode의 apply 메서드로 들어오게 됩니다. parser.parse가 눈에 보이는군요. 여기 안으로 들어가 보겠습니다.

 

 

 대충 호출 스택을 그려보면 위와 같습니다.

 

 

 그러면, GenericTokenParser 안으로 들어오게 되는데요. 여기서 중요한 것은, context의 bindings은 ContextMap입니다. 여기에는 Key값이 _parameter이고 value가 Usert 객체가 들어있습니다.

 

 

 이 상황에서, 69번째 줄로 가 보겠습니다. expression.toString은 id라던지, pw와 같은 인자로 호출이 되는 것을 담고 있습니다.

 

 

 이것은 TextSqlNode의 handleToken이 받게 되는데요. "id"라는 값과 binding 된 정보들, 이렇게 2개의 인자를 OgnlCache의 메서드인 getValue로 넘겨주면, 신기하게도, 제가 parameter의 id 값이 출력이 됩니다.

 

 

 이는, 값을 trace 해 보면 알 수 있습니다. 다음에 checkInjection을 호출하게 되는데요.

 

 

 injectionFilter가 null값으로 설정되어 있기 때문에 무난하게 통과할 수 있습니다.

 

 

 그러면 그대로 ${id} 라는 값을 ' or 1 = 1 -- 로 대치가 가능합니다.

 

 

 다음에 pw는 어떨까요? 이 값은 "1234"였습니다.

 

 

 TextSqlNode의 handleToken을 호출합니다. content는 당연하게도, "pw"입니다. 77번째 줄을 수행하고 난 뒤에, srtValue의 값은 "1234"가 나옵니다. 이것을 ${pw} 부분에 그대로 대치시킨다는 이야기입니다. 단순히 ${id} 부분을 parameter의 필드 id 값으로 대치를 하고, ${pw}를 필드 pw로 대치를 하기 때문에, sql injection에 취약할 수 밖에 없습니다.

 


 이와는 별개로, GenericTokenParser는 코딩 테스트에서 파싱 문제들이 나왔을 때 어떤 식으로 처리해야 할 지 배우기 매우 좋은 예 중 하나입니다. 사실, 저는 ${} 이런 식으로 썼을 때 mybatis에서 어떤 식으로 처리하는 지 보는 목적도 있었습니다. 하지만, 어떠한 패턴을 특정한 값으로 대치시키는 문제는 생각보다 굉장히 많이 나옵니다. 이것까지 챙겨 가시면 금상 첨화일 듯 싶습니다. 먼저 제가 설명한 클래스인 GenericTokenParser 라는 친구가 해야되는 일을 다시 생각해 보겠습니다.

 

 

 이게 다입니다. 그러면 우리는 어떤 작업을 먼저 해야 할까요? escaping된 $라던지, }는 무시해 보도록 하겠습니다.

 

 

 그러면, text에서 "${"를 찾으면 될 겁니다. 맨 처음 위치를 말입니다. 만약에 찾으면, 어떻게 하면 좋을까요? offset을 최초에 찾은 위치로부터 2, 그러니까 openToken.length()만큼 더하면 될 겁니다.

 

 

 40번째 줄이 그러한 역할을 수행합니다. 만약에 ${가 있었다면 어떻게 될까요?

 

 

 그러면, }을 찾으면 될 겁니다. 단, escape가 앞에 붙은 {이나, }은 일단 무시하자고 했습니다. 그러면 offset으로부터 보았을 때, }가 등장하는 최초의 위치를 찾아보면 됩니다.

 

 

 일단, 53번째 줄의 if 블록은 무시하고 보겠습니다. 그러면, closeToken이 나타나는 위치인 end를 찾는데요. 59번째의 append 메서드의 의미는 src의 offset 위치로부터, 연속적으로 end - offset개만큼을 expression에 append 하겠다는 의미입니다.

 

 

 그러면, exp는 요래 빠졌음을 알 수 있습니다. builder는? 일단 우리가 ${을 찾은 시점에 expression을 생성하기 위해서 }를 찾았습니다. builder는 {을 찾은 것과 관련이 있는데요. 처음에 offset은 0입니다. 

 

 

 그러니, builder는 처음에 ${을 만나기 전 까지 문자열이 append가 됩니다.

 

 

 다음에 ${을 만나면 offset이 ${을 찾은 위치에서 2만큼을 더 간 위치로 이동합니다. 당연하게도 거기서부터 }를 찾을 겁니다. 만약에 찾았다면, 찾은 위치인 end에서 offset만큼 뺀 값만큼을 offset으로부터 취합니다. 위 예제에서는 id가 나올 겁니다. 이 context 정보를 가지고, 값을 찾아낸 다음에 builder의 뒤에 append를 시킵니다.

 

 

 그 다음에는 어떻게 할까요? } 다음 위치부터 볼 겁니다. {가 나올 때 까지 찾을 겁니다. 그러면 계속 41번째에 있는 while(start>-1) 루프를 돌 겁니다. 그러다가 }가 나왔는데 {를 못 찾으면 어떻게 하면 될까요? 만약에 }가 나왔다면, offset은 }의 다음 위치로 이동할 겁니다.

 

 그러면 offset의 위치로부터, 끝까지를 builder에 append 시키면 됩니다. 이는 76번째 줄에 나와있습니다. 그런데, '\\' 조건은 무엇일까요? 왜 ${나 }가 나온 경우에 앞에 '\\'이 붙은 게 중요할까요? 이는 escaping 때문에 그렇습니다. sql 문을 작성할 때 우리는 id = '123' 이렇게 작성할 수도 있지만, '1}3' 이런 식으로도 작성할 수 있기 때문입니다.

 

 즉, mybatis에서 특수한 것처럼 취급되, 예를 들어 parameter에서 닫는 버킷 역할을 하는 '}'하고, 그냥 문자 역할을 하는 '}'는 구분이 되어야 합니다. 따라서 '\\' 조건이 있습니다. 이에 대한 처리를 따로 하기 위해서, 중간 중간에 while loop가 있습니다. 이 메서드에서 많이 나온 indexOf나, StringBuilder의 append는 상당히 많이 쓰이는 것들 중 하나이므로 익숙해 지면 좋겠습니다.