워드프레스 커버 이미지 버그

스튜디오 JT의 정재철 님이 제보하신 버그

증상 재현

  • 멀티사이트, 에디터 이하의 권한으로 글 작성, 블록 에디터 편집 중 발생.
  • ‘이미지’ 블록을 선택해 ‘현재 미디어 URL’을 다음과 같은 URL로 변경한다.
    • https://images.unsplash.com/photo-1591273703337-fbede2a94c25?ixid=M3w0NzI4ODN8MHwxfHNlYXJjaHwxMnx8R3llb25nYm9rZ3VuZ3xlbnwwfHx8fDE3MTA3NDkwNDd8MA&ixlib=rb-4.0.3&w=1816&h=800&fit=crop
    • 포인트는 이미지 URL 에 쿼리 스트링으로 & 가 들어가야 한다는 것.
  • ‘커버’ 블록으로 바로 변환한다.
  • 저장하면, 저장은 되지만 프론트로 가 보면 이미지가 깨져 있다.
  • 다시 편집 화면으로 돌아오면 블록이 정상적으로 나오지 않는다.

원인

unfiltered_html 권한과 더불어 /wp-includes/kses.php 파일의 safecss_filter_attr() 함수에서의 style 속성 처리에서 약간의 문제가 겹쳐 일어남. 앞서 포인트에서 지적했듯 URL에 & 처럼 html entity 사용시 발생.

우선 발생 조건을 다시 요악하면,

  1. 싱글 사이트에서 에디터의 권한 중 unfiltered_html 권한을 강제로 제외시키거나, 멀티사이트로 전환한다. Roles and Capabilities 문서에 따르면 에디터는 멀티사이트에서 unfiltered_html 권한을 가지지 않는다.
  2. set_current_user 액션 때 , 편집하는 사용자가 unfiltered_html 권한을 가지지 못하면 엄격한 kses 체크를 받도록 kses_init_filters() 함수가 동작한다.
  3. kses_init_filters() 함수 안에,
    add_filter( 'content_save_pre', 'wp_filter_post_kses' );
    라는 부분이 있다. 포스트 저장 시, kses 발동시키는 부분이다.
  4. kses 동작 중 wp_kses_attr_check() > safecss_filter_attr() 함수로 호출이 이어지고, safecss_filter_attr() 함수 안에서 문제가 일어난다.

에디터에서 background-image: url();을 쓰는 것은 전혀 문제가 없는데, 이미지 URL에 파라미터가 붙게 되면, 가령 파라미터를 포함한 이미지 URL이 https://sample.image.com/flower.jpg?a=foo&b=bar 처럼 되어 있었다고 치자 (CDN인 경우 종종 이런 경우가 발생). 그러면 이것이 POST 전송되면서 https://sample.image.com/flower.jpg?a=foo&b=bar로 강제로 변경된다.

그런데 safecss_filter_attr() 함수에서 HTML 태그 style 속성을 나눌 때 단순히 ‘;’ 만을 기준으로 나누게 구현되어 있다.

function safecss_filter_attr( $css, $deprecated = '' ) {  
    if ( ! empty( $deprecated ) ) {  
       _deprecated_argument( __FUNCTION__, '2.8.1' ); // Never implemented.  
    }
    ...
    $css_array = explode( ';', trim( $css ) );Code language: PHP (php)

그러므로 여기서 CSS 속성들이 잘못 분리된다. 그래서 background-image 속성이 손상되고, 따라서 저장 이후에는 올바르게 블록이 불러와지지 않는 문제가 발생한다. 다만 블록 자체는 해당 URL을 잘 기억하고 있어, 복구가 가능하다. 이는 블록의 속성값들이 보통 post_content 에 HTML 주석 형태로 저장되기 때문으로 주석에서는 이런 URL 필터링이 동작하기 않기 때문이다.

해결책

  • 옵션 1: 멀티사이트에서 계정에게 unfiltered_html 권한을 특별히 부여한다.
  • 옵션 2: url() 부분에서 & 같은 게 나오지 않도록 URL을 관리한다.
  • 옵션 3: 필터를 사용해 강제로 url() 에 & 같은 것을 &로 변환한다. PHP html_entity_decode() 함수가 도움이 될 것이다.

대충 이렇게 mu-plugin으로 구현한다.

<?php  
/**
 * mu-plugin 디렉토리에 resolve-url.php 로 저장.
 */
function resolve_url_set_current_user() {  
    if ( ! current_user_can( 'unfiltered_html' ) ) {  
       add_filter( 'pre_kses', 'resolve_url_fix_url', 9 );  
    }  
}  

function resolve_url_fix_url( $data ) {  
    return preg_replace_callback( '/url\(([^)]+)\)/', 'resolve_url_replace', $data );  
}  

function resolve_url_replace( $matches ) {  
    if ( isset( $matches[1] ) ) {  
       $parsed = parse_url( $matches[1] );  
       if ( isset( $parsed['query'] ) && str_contains( $parsed['query'], ';' ) ) {  
          $decoded = html_entity_decode( $matches[1], );  
          return "url($decoded)";  
       }  
    }  
    return $matches[0];  
}  

add_action( 'set_current_user', 'resolve_url_set_current_user' );
Code language: PHP (php)

댓글 남기기