스튜디오 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 사용시 발생.
우선 발생 조건을 다시 요악하면,
- 싱글 사이트에서 에디터의 권한 중
unfiltered_html
권한을 강제로 제외시키거나, 멀티사이트로 전환한다. Roles and Capabilities 문서에 따르면 에디터는 멀티사이트에서unfiltered_html
권한을 가지지 않는다. set_current_user
액션 때 , 편집하는 사용자가unfiltered_html
권한을 가지지 못하면 엄격한 kses 체크를 받도록kses_init_filters()
함수가 동작한다.kses_init_filters()
함수 안에,add_filter( 'content_save_pre', 'wp_filter_post_kses' );
라는 부분이 있다. 포스트 저장 시, kses 발동시키는 부분이다.- 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() 에
&
같은 것을&
로 변환한다. PHPhtml_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)