WordPress соответствующие URL с конечными тильдами

11

Мне передали отчет об уязвимости (1), который, по-видимому, подразумевает, что может быть проблема безопасности в том, как Wordpress обрабатывает URL-адреса со следующими тильдами. Кажется, сканер считает, что веб-сайт может обслуживать некоторые списки каталогов и тому подобное.

Я был удивлен тем, что мой веб-сайт по-прежнему размещал контент по этим различным URL-адресам, поэтому я провел тест, установив полностью пустой экземпляр WP, переключившись на постоянные ссылки «Опубликовать имя», и подтвердил, что да, любой URL-адрес с добавленной тильдой по-прежнему интерпретируется как URL без тильды.

Действительно, URL-адрес, как это:

https://mywordpresssite.com/my-permalink

Также доступен со следующими URL:

https://mywordpresssite.com/my-permalink~
https://mywordpresssite.com/my-permalink~/
https://mywordpresssite.com/my-permalink~~~~~~

Я немного покопался, чтобы увидеть, где WP анализирует постоянные ссылки, и отследил это class-wp.phpв parse_requestметоде, но не смог продвинуться дальше.

Мой вопрос заключается в том, предназначено ли это поведение для WP, и если да, то есть ли способ отключить его, чтобы тильды не совпадали? Зачем WP интерпретировать URL-адреса с тильдами как URL-адреса без них?

(1) Да, теперь мы все видели пару крупных взломов и утечек данных в Великобритании, это то время, когда ребята из «службы безопасности» делают вид, что делают свое дело, передавая нам разработчики отчеты о сканировании на 200 страниц полный ложных срабатываний и общих проблем, о которых они ничего не знают, ожидая, что если мы прочтем и выполним этот доклад, ничего плохого не произойдет.

dKen
источник

Ответы:

13

Давай просто

Если я хорошо понимаю OP, ваша проблема в том, что URL-адреса, содержащие тильду, совпадают вообще.

Все остальные ответы сосредоточены на том факте, что очистка для запроса удаляет некоторые символы перед выполнением запроса, однако следует иметь возможность предотвратить несоответствие правила перезаписи при некоторых обстоятельствах.

И это выполнимо, не очень легко, но выполнимо.

Почему это соответствует, в первую очередь?

Причина, по которой двум URL-адресам нравятся example.com/postnameи example.com/postname~совпадают одно и то же правило перезаписи, заключается в том, что в правиле перезаписи WP для записей используется тег перезаписи, %postname%который заменяется регулярным выражением ([^/]+)при создании правил перезаписи.

Проблема в том, что регулярное выражение ([^/]+)также совпадает с именем поста, postname~и из-за санации запрашиваемое имя будет postnameзаканчиваться правильным результатом.

Это означает, что если мы сможем изменить регулярное выражение с ([^/]+)на ([^~/]+)тильду, мы больше не будем совпадать, поэтому мы активно запрещаем сопоставление URL-адресов, содержащих тильду в имени поста.

Так как ни одно правило не будет соответствовать, URL-адрес в конечном итоге будет 404, что, я думаю, должно быть ожидаемым поведением.

Запретить сопоставление

add_rewrite_tagэто функция, которая, несмотря на свое имя, может использоваться для обновления существующего тега перезаписи, например %postname%.

Итак, если мы используем код:

add_action('init', function() {
  add_rewrite_tag( '%postname%', '([^~/]+)', 'name=' );
});

мы достигнем нашей цели и неexample.com/postname~ будем соответствовать правилу для .example.com/postname

Итак, да, 3 строки выше - это единственный код, который вам понадобится .

Однако, прежде чем это сработает, вам нужно сбросить правила перезаписи, посетив страницу настроек постоянной ссылки на сервере.

Обратите внимание, что регулярные выражения ([^~/]+)запрещают использование тильды где-либо в имени сообщения, не только как завершающий символ, но так как имена сообщений не могут содержать тильду из-за очистки, что не должно быть проблемой.

Gmazzap
источник
1
+1 как простота ;-) похоже, что мы могли бы настроить это и для других шумовых символов.
Birgire
1
@ мы не все? ;)
gmazzap
@birgire да, мы могли бы предотвратить удаление любого символа sanitize_title, но так как он фильтруется, невозможно написать всегда верное решение. Так что я пошел конкретно.
gmazzap
1
Этот ответ имеет самое чистое решение и четко объясняет проблему, с которой мы сталкиваемся. Большое спасибо - щедрость вам!
dKen
7

предназначено поведение для WP

Да, как уже объяснялось, WP_Query::get_posts()использует sanitize_title_for_query()( который используетsanitize_title() ) для очистки названия поста единственного поста.

Короче, после того, как название поста прошло sanitize_title_for_query(), так my-permalink === my-permalink~~~как sanitize_title_for_query()убирает трейлинг ~~~. Вы можете проверить это, выполнив следующие действия:

echo  sanitize_title_for_query( 'my-permalink~~~' )

Есть ли способ, которым я могу отключить это, чтобы тильды не совпадали

Это не то, что вы можете отключить. В sanitize_title()вызванном фильтре есть фильтр, sanitize_titleкоторый вы можете использовать для изменения поведения sanitize_title(), но это почти всегда не очень хорошая идея. SQL-инъекция очень серьезна, поэтому, если что-то проскользнет из-за плохой санитарии, это может серьезно повлиять на целостность вашего сайта. «Из-за санитарии» иногда может быть боль в заднице.

Я не уверен, что вы после этого, но я подозреваю, что вы, возможно, хотите 404 отдельных сообщений с этим тильда, по вашим словам, "выключить его". На этом этапе я могу думать только о том, чтобы остановить основной запрос, когда у нас есть эти тильды. Для этого мы можем отфильтровать posts_whereпредложение основного запроса.

ФИЛЬТР

Примечание: я рассматривал только обычные отдельные сообщения, а не статические первые страницы или вложения, вы можете расширить фильтр, чтобы включить это

add_filter( 'posts_where', function ( $where, \WP_Query $q )
{
    // Only apply the filter on the main query
    if ( !$q->is_main_query() )
        return $where;

    // Only apply the filter on singular posts
    if ( !$q->is_singular() )
        return $where;

    // We are on a singular page, lets get the singular post name
    $name = sanitize_title_for_query( $q->query_vars['name'] );

    // Suppose $name is empty, like on ugly permalinks, lets bail and let WorPress handle it from here
    if ( !$name )
        return $where;

    // Get the single post URL
    $single_post_url = home_url( add_query_arg( [] ) );
    $parsed_url      = parse_url( $single_post_url );

    // Explode the url and return the page name from the path
    $exploded_pieces = explode( '/',  $parsed_url['path'] );
    $exploded_pieces = array_reverse( $exploded_pieces );

    // Loop through the pieces and return the part holding the pagename
    $raw_name = '';
    foreach ( $exploded_pieces as $piece ) {
        if ( false !== strpos( $piece, $name ) ) {
            $raw_name = $piece;

            break;
        }
    }

    // If $raw_name is empty, we have a serious stuff-up, lets bail and let WordPress handle this mess
    if ( !$raw_name )
        return $where;

    /**
     * All we need to do now is to match $name against $raw_name. If these two don't match,
     * we most probably have some extra crap in the post name/URL. We need to 404, even if the
     * the sanitized version of $raw_name would match $name. 
     */
    if ( $raw_name === $name )
        return $where;

    // $raw_name !== $name, lets halt the main query and 404
    $where .= " AND 0=1 ";

    // Remove the redirect_canonical action so we do not get redirected to the correct URL due to the 404
    remove_action( 'template_redirect', 'redirect_canonical' );

    return $where;
}, 10, 2 );

Несколько замечаний

Приведенный выше фильтр вернет страницу 404, когда у нас будет такой URL https://mywordpresssite.com/my-permalink~~~~~~. Однако, удалив remove_action( 'template_redirect', 'redirect_canonical' );из фильтра, можно автоматически перенаправить запрос https://mywordpresssite.com/my-permalinkи отобразить один пост, благодаря redirect_canonical()которому подключается тот, template_redirectкоторый обрабатывает перенаправление WordPress, созданного 404-ми.

Питер Гусен
источник
7

Да, кажется странным, что у нас должно быть одинаковое совпадение для:

example.tld/2016/03/29/test/

и, например,

example.tld/2016/03/29/..!!$$~~test~~!!$$../

Почему это возможно, кажется, эта часть из WP_Query::get_posts()метода:

if ( '' != $q['name'] ) {
    $q['name'] = sanitize_title_for_query( $q['name'] );

где sanitize_title_for_query()определяется как:

function sanitize_title_for_query( $title ) {
        return sanitize_title( $title, '', 'query' );
}

Должна быть возможность сделать это более строгим с помощью sanitize_titleфильтра, но не стоит переопределять вывод по умолчанию, основанный на том sanitize_title_with_dashes, что отвечает за санитарию здесь. Вам следует подумать о создании заявки, а не об ее изменении, если об этом поведении уже не было ни одного тока.

Обновить

Интересно, могли бы мы очистить шум от текущего пути sanitize_title_for_query()и перенаправить на очищенный URL при необходимости?

Вот демоверсия, с которой вы можете поиграть на своем тестовом сайте и настроить ее под свои нужды:

/**
 * DEMO: Remove noise from url and redirect to the cleaned version if needed 
 */
add_action( 'init', function( )
{
    // Only for the front-end
    if( is_admin() )
        return;

    // Get current url
    $url = home_url( add_query_arg( [] ) );

    // Let's clean the current path with sanitize_title_for_query()
    $parse = parse_url( $url );
    $parts = explode( '/',  $parse['path'] );
    $parts = array_map( 'sanitize_title_for_query', $parts );   
    $path_clean = join( '/', $parts );
    $url_clean = home_url( $path_clean );
    if( ! empty( $parse['query'] ) )
        $url_clean .= '?' . $parse['query'];

    // Only redirect if the current url is noisy
    if( $url === $url_clean )
        return;
    wp_safe_redirect( esc_url_raw( $url_clean ) );
    exit;
} );

Возможно, даже лучше использовать sanitize_title_with_dashes()напрямую, чтобы избежать фильтров и заменить:

$parts = array_map( 'sanitize_title_for_query', $parts );

с:

foreach( $parts as &$part )
{
    $part = sanitize_title_with_dashes( $part, '', 'query' );
}

PS: я думаю, что я изучил этот трюк, чтобы получить текущий путь с пустым add_query_arg( [] ), из @gmazzap ;-) Это также отмечено в Кодексе. Еще раз спасибо @gmazzap за напоминание об использовании esc_url()при отображении вывода add_query_arg( [] )или, esc_url_raw()например, при перенаправлении его. Проверьте предыдущую ссылку на Кодекс для этого тоже.

birgire
источник
+1 Просто чтобы уточнить, эти специальные символы удаляются, поэтому, хотя в адресной строке отображается странная версия URL, WordPress работает с реальным URL, поэтому запрос работает в первую очередь. Я не вижу никаких угроз безопасности мэра с таким поведением.
Николай
1
да, я думаю, что мы не должны связываться с фильтром очистки, чтобы изменить этот @ialocin
birgire
1
Конечно, если нет очень веских причин, это не стоит хлопот. Не сказать, что это, скорее всего, плохо для здравомыслия разработчиков - даже не вдаваясь в техническую санитарию. Только мои два цента, хотя.
Николай
1
@birgire при таком использовании add_query_argнужно избегать esc_urlили esc_url_rawпредотвращать проблемы безопасности ...
gmazzap
Ах, да, спасибо, если я правильно помню, это была проблема безопасности, обнаруженная во многих плагинах недавно @gmazzap
birgire
3

Позвольте мне объяснить обработку запроса в WordPress и метод изменения поведения WordPress для достижения ваших целей.

Разбор запроса

Когда WordPress получает запрос, он начинает процесс разбора запроса и преобразования его в страницу. Ядро этого процесса начинается, когда WP::main()вызывается основной метод запроса WordPress . Эта функция анализирует запрос, как вы правильно определили, в parse_request()(in includes/class-wp.php). Там WordPress пытается сопоставить URL с одним из правил перезаписи . Когда URL-адрес совпадает, он создает строку запроса из частей URL-адреса и кодирует эти части (все между двумя слешами) urlencode(), чтобы предотвратить использование специальных символов, таких как &путаница в строке запроса. Эти закодированные символы могли заставить вас думать, что проблема заключалась в этом, но они фактически превращаются в соответствующие им «настоящие» символы при разборе строки запроса.

Выполнение запроса, связанного с запросом

После того, как WordPress проанализировал URL-адрес, он устанавливает основной класс запросов WP_Query, что делается в том же main()методе WPкласса. Основу WP_Queryможно найти в его get_posts()методе, где все аргументы запроса анализируются и обрабатываются, а фактический запрос SQL создается (и, в конце концов, запускается).

В этом методе в строке 2730 выполняется следующий код:

$q['name'] = sanitize_title_for_query( $q['name'] );

Это очищает сообщение для его извлечения из таблицы сообщений. Вывод отладочной информации внутри цикла показывает, что именно в этом заключается проблема: имя вашего сообщения my-permalink~, преобразуется в my-permalink, которое затем используется для извлечения сообщения из базы данных.

Функция очистки заголовка поста

Функция sanitize_title_for_queryвызывается sanitize_titleс правильными параметрами, после чего происходит санация заголовка. Теперь ядро ​​этой функции применяет sanitize_titleфильтр:

$title = apply_filters( 'sanitize_title', $title, $raw_title, $context );

Этот фильтр, в родном WordPress, одна функция прилагается к нему: sanitize_title_with_dashes. Я написал обширный обзор того, что делает эта функция, который можно найти здесь . В этой функции строка, вызывающая вашу проблему

$title = preg_replace('/[^%a-z0-9 _-]/', '', $title);

Эта строка удаляет все символы, кроме буквенно-цифровых символов, пробелов, дефисов и подчеркиваний.

Решение вашей проблемы

Таким образом, существует в основном единственный способ решить вашу проблему: удалить sanitize_title_with_dashesфункцию из фильтра и заменить ее собственной функцией. На самом деле это не так сложно сделать, но :

  1. Когда WordPress изменит внутренний процесс очистки заголовков, это будет иметь серьезные последствия для вашего сайта.
  2. Другие плагины, подключаемые к этому фильтру, могут некорректно обрабатывать новые функции.
  3. Самое главное : WordPress использует результат sanitize_titleфункции непосредственно в запросе SQL по следующей строке:

    $where .= " AND $wpdb->posts.post_name = '" . $q['name'] . "'";

    Если вы когда-нибудь захотите изменить фильтр, убедитесь, что вы правильно экранировали заголовок, прежде чем он будет использован в запросе!

Вывод: решение вашей проблемы не является необходимым в плане безопасности, но если вы хотите это сделать, замените его sanitize_title_with_dashesсобственной функциональностью и обратите внимание на экранирование SQL.

Примечание: все имена файлов и номера строк соответствуют файлам WordPress 4.4.2.

Engelen
источник
3

Некоторые люди уже объяснили проблему, поэтому я просто опубликую альтернативное решение. Должно быть довольно очевидным.

add_action( 'template_redirect', function() {
    global $wp;

    if ( ! is_singular() || empty( $wp->query_vars['name'] ) )
        return;

    if ( $wp->query_vars['name'] != get_query_var( 'name' ) ) {
        die( wp_redirect( get_permalink(), 301 ) );
        // or 404, or 403, or whatever you want.
    }
});

Вы должны сделать что - то немного по- другому для иерархических типов почтовых , хотя, так как WP_Queryбудет проходить pagenameчерез , wp_basenameа затем дезинфицировать его, так query_vars['pagename']и get_query_var('pagename')не будет соответствовать для детей becuase последний не будет содержать родительскую часть.

Я хотел бы redirect_canonicalпросто позаботиться об этом дерьме.

КОВШЕНИН
источник
0

ЭТО ИСПРАВЛЕНИЕ ... ДЛЯ УСТРАНЕНИЯ WORDPRESS ПРОСТО ДОБАВЬТЕ НАЧАЛО БЕЗОПАСНОГО мода, установленного над блоком, сгенерированным Wordpress.

# BEGIN security mod
<IfModule mod_rewrite.c>
RewriteRule ^.*[~]+.*$ - [R=404]
</IfModule>
#END security mod

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /wordpress/
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /wordpress/index.php [L]
</IfModule>

# END WordPress
Майкл С. Ховард
источник
-3

Вы всегда можете попробовать добавить следующее в ваш .htaccessфайл:

RewriteEngine On
RewriteRule \.php~$  [forbidden,last]

Вторая строка выше должна идти прямо под первой показанной строкой. Это должно препятствовать index.php~отображению в URL.

Huginn
источник
Это не работает для милых постоянных ссылок, о которых вопрос, верно?
Николай