Extra Form
PHP PHP 7.4
CMS Rhymix 2.0

다음과 같은 커스텀 쿼리를 통해 동적 쿼리를 실행하려 하고 있습니다.

$columns = implode(',', $this->columnList);
$tables = 'documents';
$conditions = 'documents.module_srl = ?';
$cond_args = array($args->module_srl);

foreach ( $args->statusList as $key => $val )
{
    if ( $key === 0 )
    {
        $conditions .= ' AND documents.status IN (?';
    }
    else
    {
        $conditions .= ', ?';
    }
    if ( $key === count($args->statusList) - 1 )
    {
        $conditions .= ')';
    }
    $cond_args[] = $val;
}

// (중간 생략) ~~~~ 각 변수들에 내용을 추가해서 동적 쿼리 생성

$navigation = ' ORDER BY ' . $args->sort_index . ' '. strtoupper($args->order_type) .' LIMIT ' . $args->list_count;

$query = 'SELECT ' . $columns . ' FROM ' . $tables . ' WHERE ' . $conditions . $navigation;

$oDB = DB::getInstance();
$stmt = $oDB->query($query, $cond_args);
$result = $stmt->fetchAll();

debugPrint($result);

 

위와 같이 하면 $result에 목록은 잘 담깁니다.

다만 하이라이트한 6~21행처럼 status 컬럼으로부터 PUBLIC, SECRET, TEMP 등의 문서 상태를 가져오는 부분이 너무 발코딩이어서요ㅜ

 

https://github.com/rhymix/rhymix/pull/1332

여기에 있는 커스텀 쿼리 부분을 참고해서 하긴 했는데 이런 식으로 해도 되나요?

혹시 더 현명한 코드가 있을지 여쭙습니다.

아니면 제가 모르는 다른 문법이나 메소드가 더 있을지요.

  • Lv37

    커스텀 쿼리 방식은 제대로 따라하신 것이 맞습니다. 테이블 접두사 붙일 필요 없는 것까지 잘 파악하셨네요.

     

    IN ( ... ) 쿼리는 XML 쿼리 문법을 사용하는 것이 압도적으로 편리합니다. operation="in"이라고 선언해 놓고 배열을 그대로 넘겨주기만 하면 알아서 SQL을 작성해 주니까요. (옛날처럼 쉼표로 구분해서 넘겨주는 방식도 지원하지만, 권장하지 않습니다. 어차피 다시 뜯어서 배열로 만듭니다. 비효율적이죠...)

     

    굳이 IN ( ... ) 부분을 직접 작성하고 싶으시다면 아래와 같이 하는 것이 그나마 깔끔합니다.

    $conditions .= ' AND documents.status IN (';
    $conditions .= implode(',', array_fill(0, count($args->statusList), '?'));
    $conditions .= ')';
    $cond_args = [...$cond_args, ...$args->statusList];  // PHP 7.4 미만은 array_merge() 또는 루프 사용

     

    그런데 쿼리문 작성에 관여하는 $this->columnList, $args->sort_index, $args->order_type, $args->list_count 등의 변수들 중 사용자 입력값에 영향을 받을 만한 것이 하나라도 있다면 SQL 인젝션 공격에 당할 수 있습니다. 그나마 LIMIT 뒤의 숫자는 물음표로 치환할 수 있지만, 컬럼명이나 정렬 방식은 prepared statement 처리가 불가능하기 때문에 언제나 위험에 노출되어 있습니다. 어떤 이유로든 SQL문에 PHP 변수를 끼워넣고 있다면 매우 높은 확률로 잘못하고 있는 것입니다.^^

     

    결론: XML 쿼리 쓰세요.

  • Lv37 Lv19

    감사합니다. 권해주신 코드는 깔끔하게 잘 됩니다!

    안 그래도 sort_index, order_type은 ?로 치환이 안 되는 것 같더라구요.
    그래서 쿼리문에 직접 적용을 했는데, 결국 인젝션 공격의 구멍이 될 수 있군요. 흠..
    컬럼명은 *으로 바꿔주면 될 것 같은데, 정렬 방식은 대체 방법이 없네요...

    XML쿼리 대신 커스텀 쿼리를 짜는 목적은 동적 쿼리문을 만들어야 하는 상황이어서요.
    원글의 소스에서 '중간 생략'된 부분인데, url을 통해 들어온 변수를 통해 WHERE절을 탄력적으로 바꾸고자 합니다.
    근데 XML쿼리로는 동적인 대응이 불가능한 것으로 이해하고 있어서요. 그렇지 않나요?

    제 능력으로는ㅜ 커스텀 쿼리로는 정렬 방식 지정에서 인젝션 공격을 회피할 방법이 없고, XML쿼리로는 동적인 대응이 불가능한 상황 같은데ㅜ
    이럴 땐 그냥 깔끔하게 포기해야죠, 뭐ㅠㅠ

  • Lv37 Lv19

    아, $args->sort_index나 $args->order_type이 지정된 문자열 이외의 것이 들어왔을 때
    특정한 문자열로 고정시켜주면 인젝션에 방어할 수 있는 건가요?

     

    가령 _setSortIndex 함수와 같은 식으로요.

    https://github.com/rhymix/rhymix/blob/922025ae8ed5de58dd75d1cac20494eb25153f23/modules/document/document.model.php#L1290

     

    만약 그렇다면 원글의 코드가 _setSortIndex 실행 이후 시점인 document.getDocumentList의 before 에서 트리거로 들어가는 것이어서

    https://github.com/rhymix/rhymix/blob/922025ae8ed5de58dd75d1cac20494eb25153f23/modules/document/document.model.php#L250

    나름 방어가 되지 않을까 하는 생각이 들었습니다.

     

    order_type은 

    $args->order_type = (isset($obj->order_type) && $obj->order_type === 'desc') ? 'desc' : 'asc';

    이것으로 될 것 같기도 하구요.

  • Lv19 Lv37

    동적인 대응이 가능하다는 것이 XML 쿼리의 가장 큰 장점 중 하나인데요. ㅎㅎ

    <condition>에 사용할 var="" 변수를 전달하지 않으면 해당 <condition>이 자동으로 삭제되니까, 다양한 상황에 대비한 <condition>을 여러 개 선언해 두고 그때그때 필요한 것만 작동하도록 할 수 있잖아요.

     

    라이믹스 2.0에서는 var="" 변수의 유무와 관계없이 또다른 변수를 사용하여 해당 부분의 적용 여부를 컨트롤할 수 있는 ifvar="" 문법이 추가되어서 더욱 다양한 응용이 가능합니다. 커스텀 쿼리 기능 만들어 놓고 정작 커스텀 쿼리를 사용할 핑계를 없애버리는 전략이죠!^^

  • Lv19 Lv37
    네, 그런 식으로 일일이 막을 수는 있습니다.^^
  • Lv37 Lv19
    그러고 보니 몇몇 xml에서 봤던 것 같아요.
    이게 뭔가 하고 그냥 넘어갔었는데, 구글링을 해보니 이런 것인가 보군요 :)
    https://github.com/rhymix/rhymix/commit/6b7486e74f63e1470faac636d896ad6dd318f06f

    짧은 실력이어서 한참 걸리겠지만 그래도 공부해보겠습니다!
  • Lv24
    애드온에서 구현이 안되는 부분을 외부 PHP파일을 다시 불러오는 방식으로 뭔가를 쿼리하시는 것 같은데.. 애드온도 XML쿼리가 잘되니까.. XML 쿼리 써주세요..ㅋㅋㅋ

    아에 외부로 구현되어야 할 것이라면 모듈을 만들어서.. 해당 기능을 담당하게끔 해주는게 더 낫죠..(그래서 모듈로는 언제쯤??ㅋㅋㅋ)
  • Lv24 Lv19
    앗, 외부 php 가져오는 건 아니구요ㅋㅋ
    쿼리문을 동적으로 생성하고자 해서요.

    주소창에 &extra_vars8=세미나실&extra_vars9=윤삼 같은 게 달라붙게 되는데
    XML 쿼리로는 이렇게 다양한 변수들을 단번에 포착할 방법이 없어서요;;
    그렇다고 extra_vars8 가지고 xml 쿼리 짜고, 또 따로 extra_vars9로 xml 쿼리를 짜서 output 끼리 병합하거나 교차시키는 건 어딘지 비효율적인 것 같아서요.

    그나저나 모듈은 정서적 안정을 찾은 다음쯤요? ㅋㅋㅋㅋ
  • Lv19 Lv24
    스케줄 모둘 수정하신 정도면 뭐 그럴 필요도 없으실텐데요 ㅋㅋ
  • ? Lv10
    sql 인젝션 방어는 mysqli_real_escape_string() 열심히 잘 호출해주면 되는거였던걸로...
    ? 로 해서 치환하는 것도 내부적으론 에스케이프 처리 하는거죠....
  • ? Lv10 Lv37

    라이믹스 2.0부터는 PDO 기반이니까 mysqli_* 함수는 쓸 수 없지요. 비슷한 함수를 빠짐없이 사용한다 해도 문자열이 아닌 컬럼명은 전혀 방어되지 않고요.

    Prepared statement는 내부적으로 이스케이프 처리하는 거 아닙니다. 구 버전에서 쓰던 에뮬레이션 모드라면 몰라도... 쿼리문과 데이터를 서로 혼동하지 못하도록 각각 별도의 패킷으로 전송하는 방식입니다. DB서버 입장에서도 복잡한 문자열을 제외한 짧고 간결한 쿼리문을 파싱하는 것이 훨씬 빠르기 때문에, 문자열을 다시 조립하지 않고 각각의 변수가 담겨 있는 메모리 주소를 직접 참조하지요. 패킷을 나눠 보냄으로써 발생하는 오버헤드는 이러한 효율 개선으로 만회합니다.

     

    즉, 변수를 이스케이프하여 따옴표로 묶어서 쿼리문에 삽입한 상태의 문자열은 코딩하는 사람의 머릿속에만 있을 뿐, PHP나 DB서버 어느 쪽에서도 그렇게 다시 조립하는 불필요한 과정은 거치지 않습니다.

    예전이랑 많이 다릅니다.^^