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-сторінкові звіти про сканування. Повна помилкових позитивних та загальних питань, про які вони нічого не знають у очікуванні, якщо ми прочитаємо та будемо діяти на згаданому звіті, нічого поганого ніколи не станеться.

Відповіді:


13

Пройдемо просто

Якщо я добре розумію, ваша проблема полягає в тому, що URL-адреси, що містять тильду, взагалі відповідають.

Усі інші відповіді зосереджуються на тому, що санітарія для запиту викреслює деякі символи перед виконанням запиту, однак слід бути спроможним запобігти правилу перезапису, який не збігається за певних обставин.

І це виконано, не дуже легко, але виконано.

Чому це відповідає, в першу чергу?

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

Проблема полягає в тому, що регулярний вираз ([^/]+)також відповідає прізвищу, postname~і через санітарію, запитуване ім’я postnameзакінчується дійсним результатом.

Це означає, що якщо нам вдасться змінити регулярний вираз ([^/]+)на ([^~/]+)tilde, він більше не збігатиметься, тому ми активно запобігаємо збігу URL-адрес, що містять тильду в назві посади.

Оскільки жодне правило не буде відповідати, я думаю, що URL-адреса виявиться 404, що, напевно, має бути очікуваною поведінкою.

Не допускати відповідності

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

Отже, якщо ми використовуємо код:

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

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

Отже, так, 3 рядки вище - це єдиний код, який вам знадобиться .

Однак, перш ніж це працює, вам потрібно буде спробувати переписати правила, відвідавши сторінку налаштувань постійної посилання в бекенді.

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


1
+1 як простота ;-) також виглядає так, що ми могли б регулювати це також для інших шумових характеристик.
birgire

1
@birgire, чи не всі ми? ;)
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 ;-) Це також зазначено в Codex. Ще раз дякую @gmazzap за нагадування про використання esc_url()під час відображення виводу add_query_arg( [] )або, esc_url_raw()наприклад, перенаправлення його. Перевірте і попередню посилання на Codex.


+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()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.


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-адресах.


Це не працює для досить постійних посилань, про які йдеться, правда?
Миколай
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.