Опасный псевдопротокол javascript

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

Поэтому стоит посвятить этому отдельную часть для подробного рассмотрения.

Прежде чем начать, давайте ответим на вопрос из прошлой части.

В точке внедрения innerHTML теги <script> не выполняются. Однако их можно использовать совместно с iframe.

Атрибут srcdoc iframe может содержать полный HTML-код, создавая новую веб-страницу. Таким образом, ранее бесполезный тег <script> можно использовать здесь. Кроме того, поскольку это атрибут, его содержимое можно предварительно закодировать, что означает, что результат будет таким же:

document.body.innerHTML =  '<iframe srcdoc="&lt;script>alert(1)&lt;/script>"></iframe>';

Таким образом, даже если точка внедрения - innerHTML, комбинация <iframe srcdoc> и <script> может использоваться для выполнения кода.

Что такое псевдопротокол javascript:?

Слово "псевдо" относится к псевдокоду, который похож на виртуальный код.

По сравнению с "реальными протоколами" типа HTTP, HTTPS или FTP, псевдопротокол больше похож на специальный протокол, не связанный с сетью. Примеры псевдопротоколов включают mailto: и tel:.

Псевдопротокол javascript: особенен тем, что его можно использовать для выполнения JavaScript-кода.

Где можно использовать псевдопротокол javascript:?

Первое место - атрибут href, упомянутый в предыдущей части

<a href="javascript:alert(1)">Link</a>

Простое нажатие на эту ссылку вызовет JavaScript и XSS срабатывает.

Второе место - атрибут src в <iframe>:

<iframe src="javascript:alert(1)"></iframe>

В отличие от примера с <a>, для его срабатывания не требуется никакого взаимодействия с пользователем.

Наконец, атрибут action в <form> также может содержать то же самое. Атрибут formaction в <button> также аналогичен, но оба, как и <a>, требуют нажатия для запуска:

<form action="javascript:alert(1)">
    <button type="submit">Submit</button>
</form>
<form id="f2" action="javascript:alert(2)">
    <button type="submit">Submit</button>
</form>
<button form="f2">Submit</button>

Почему это опасно?

Псевдопротокол javascript: часто упускают из виду, хотя на практике его используют довольно часто.

Например, imagine website с функцией вставки видео с YouTube по ссылке, которую вводит пользователь. Если разработчик этой функции не думает о безопасности, он может написать ее так:

<iframe src="<?= $youtube_url ?>" width="500" height="300"></iframe>

Если я введу как ссылку на YouTube javascript:alert(1), это станет уязвимостью XSS. Даже если добавить проверку на наличие youtube.com в URL, ее можно обойти с помощью javascript:alert(1);console.log('youtube.com').

Правильный подход - проверить, соответствует ли URL формату видеоролика YouTube и убедиться, что он начинается с https://.

Думаете, такая функция встречается редко? Попробуйте ввести на странице Facebook ссылку на собственный ресурс. Такие функции встречаются чаще, чем кажется, верно?

Это легко упускаемая из виду область. Например уязвимость на платформе онлайн-курсов в Тайване Hahow:

Если реализация на бэкенде написана на коде, это может выглядеть так:

<a href="<?php echo htmlspecialchars($data) ?>">link</a>

Хотя символы <>" и закодированы, что предотвращает добавление тегов и выход из кавычек для добавления атрибутов, злоумышленник все равно может внедрить javascript:alert(1), поскольку в нем нет запрещенных символов.

Кроме того, современные фреймворки фронтенда обычно автоматически обрабатывают экранирование. Если вы не используете dangerouslySetInnerHTML в React или v-html в Vue, проблем быть не должно. Однако с href все по-другому по причинам, упомянутым выше - его содержимое не очищается.

Поэтому, если вы напишете в React так, это приведет к проблемам:

import React from "react";

export function App(props) {
    // Assume the following data comes from the user input
    const handleClick = (event) => {
        event.preventDefault(); // Предотвращаем переход по ссылке
        alert(1); // Выполняем нужное действие
    };
    return <a href="#" onClick={handleClick}>click me</a>;
}

Это уязвимость XSS, где выполнение кода всего в один клик.

Однако React ввел предупреждение об этом поведении в версии 16.9, и оно также задокументировано:

Устаревание URL javascript:

В сообщении о предупреждении говорится:

Будущая версия React заблокирует URL javascript: в качестве меры безопасности. Если возможно, используйте обработчики событий. Если вам нужно генерировать небезопасный HTML, попробуйте использовать dangerouslySetInnerHTML.

Будьте осторожны с псевдопротоколом javascript: и всегда думайте о потенциальных уязвимостях XSS при работе с пользовательским вводом. Используйте современные фреймворки и библиотеки, которые помогают безопасно обрабатывать ввод, и следуйте рекомендациям по написанию безопасного кода.

В этом разделе рассмотрим еще несколько аспектов опасности псевдопротокола javascript:

Дополнительные обсуждения в GitHub: 1. React@16.9 block javascript:void(0); #16592 2. False-positive security precaution warning (javascript: URLs) #16382

В Vue можно написать код, подобный этому:

<script setup>
import { ref } from 'vue';

const handleClick = (event) => {
    event.preventDefault(); // Предотвращаем переход по ссылке
    alert(1); // Выполняем нужное действие
};
</script>
<template>
  <a href="#" @click="handleClick">click me</a>
</template>

Это также успешно выполнит JavaScript-код. Данный метод атаки упоминается в документации Vue под названием "Внедрение URL" . Разработчикам рекомендуется выполнять валидацию и обработку URL на сервере, а не ждать этого от фронтенда.

Для обработки на фронтенде предлагается использовать библиотеку sanitize-url.

Перенаправление страниц также несет в себе риски

Многие сайты реализуют функцию "перенаправление после входа", которая перенаправляет пользователей на изначально запланированную страницу. Например:

const searchParams = new URLSearchParams(location.search);window.location = searchParams.get("redirect");

В чем проблема с этим кодом?

Значение window.location также может быть псевдопротоколом javascript:

window.location = "javascript:alert(document.domain)";

После выполнения приведенного выше кода вы увидите знакомое окно предупреждения. Это то, о чем должны знать фронтенд-разработчики.

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

Например, найденная уязвимость на сайте Matters News. Вот их страница входа:

После нажатия кнопки подтверждения вызывается функция redirectToTarget, код которой выглядит следующим образом:

/**
 * Redirect to "?target=" or fallback URL with page reload.
 * (works on CSR)
 */
export const redirectToTarget = ({
    fallback = "current",
}: {
    fallback?: "homepage" | "current",
} = {}) => {
    const fallbackTarget = fallback === "homepage" 
        ? `/` // FIXME: to purge cache
        : window.location.href;

    const target = getTarget() || fallbackTarget;
    window.location.href = decodeURIComponent(target);
};

После получения целевого URL, функция использует его напрямую для перенаправления: window.location.href = decodeURIComponent(target). А функция getTarget просто получает значение параметра target из строки запроса.

Таким образом, если URL входа - https://matters.news/login?target=javascript:alert(1), то при успешном входе у пользователя появится всплывающее окно, что является XSS-атакой!

Более того, XSS на странице входа имеет серьезные последствия. Она может напрямую захватить введенные значения, что означает кражу логина и пароля пользователя. Для проведения реальной атаки можно отправить фишинговые письма пользователям сайта с этой вредоносной ссылкой. Поскольку URL выглядит нормально, а целевая страница является настоящей, доверие к ней может быть высоким.

Кража логина и пароля через XSS после их ввода и входа, а также перенаправление пользователя на главную страницу без следов может привести к взлому учетной записи и ее захвату.

Подводя итог, можно сказать, что основная концепция XSS знакома многим. Однако вы можете быть не так хорошо знакомы с атаками с использованием псевдопротокола javascript:. Поэтому я хотел бы специально остановиться на этой проблеме, чтобы вы могли быть более осторожными и принимать соответствующие меры защиты при встрече с такими атрибутами.

Методы защиты

Во-первых, если у вас есть библиотеки, такие как упомянутая ранее sanitize-url, это будет идеальным решением. Хотя это может быть не полностью безопасно, библиотека широко используется и прошла обширные тестирования, поэтому многие проблемы и методы обхода, возможно, уже решены.

Вы можете обрабатывать URL самостоятельно, но давайте посмотрим, что обычно происходит, когда вы делаете это.

Поскольку вредоносная строка - javascript:alert(1), некоторые могут подумать, что достаточно проверить, начинается ли она с javascript: или убрать все вхождения javascript из строки.

Однако этот подход неэффективен, поскольку речь идет о содержимом атрибута href, а содержимое атрибута в HTML может быть закодировано. Другими словами, я могу сделать так:

<a href="&#106avascript&colon;alert(1)">click me</a>

Внутри нет содержимого, которое нам нужно фильтровать, и оно не начинается с javascript:, поэтому оно может обойти ограничение.

Лучший подход - разрешать только строки, начинающиеся с http:// или https://. Это обычно предотвращает любые проблемы. Некоторые более строгие методы включают использование JavaScript для разбора URL, например:

console.log(new URL("javascript:alert(1)"));
/*  
{    
    // ...
    href:"javascript:alert(1)",
    origin: "null",
    pathname: "alert(1)",    
    protocol: "javascript:",  
}
*/

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

Еще одна распространенная ошибка - использовать разбор URL на основе имени хоста или источника, например:

console.log(new URL("javascript:alert(1)"));
/*  
{    
    // ...    
    hostname: "",    
    host: "",    
    origin: null  
}
*/

Когда hostname или host пусты, это означает, что URL недействителен. Хотя на первый взгляд этот метод может показаться приемлемым, мы можем использовать функцию JavaScript, где // рассматривается как комментарий, в сочетании с символами новой строки, чтобы создать строку, которая выглядит как URL, но на самом деле является псевдопротоколом javascript::

console.log(new URL("javascript://example.com/%0aalert(1)"));

/*  
{    
    // ...    
    hostname: "",    
    host: "",    
    origin: null  
}
*/

Хотя это похоже на URL, он отлично работает в Chrome без каких-либо проблем или ложных срабатываний. Однако Safari ведет себя по-другому. При выполнении того же кода в Safari 16.3 вывод:

console.log(new URL("javascript://example.com/%0aalert(1)"));

/*  
{    
    // ...    
    hostname: "example.com",
    host: "example.com",
    origin: "null"  
}
*/

В Safari он успешно анализирует имя хоста и хост. Кстати, я узнал этот трюк из твита Masato.

Если вы действительно хотите использовать регулярное выражение для проверки, является ли это псевдопротоколом javascript:, вы можете обратиться к реализации в исходном коде React (многие библиотеки используют аналогичное регулярное выражение):

// A javascript: URL can contain leading C0 control or \u0020 SPACE,// and any newline or tab are filtered out as if they're not part of the URL.// 
https://url.spec.whatwg.org/#url-parsing//
 Tab or newline are defined as \r\n\t:// 
https://infra.spec.whatwg.org/#ascii-tab-or-newline//
 A C0 control is a code point in the range \u0000 NULL to \u001F// INFORMATION SEPARATOR ONE, inclusive:// 
https://infra.spec.whatwg.org/#c0-control-or-space/*
 eslint-disable max-len */const isJavaScriptProtocol =  /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i;

Из приведенного регулярного выражения можно увидеть гибкость javascript:. Он может иметь дополнительные символы перед началом и даже неограниченное количество символов новой строки и табуляции внутри строки. Вот почему я говорил, что самостоятельно обрабатывать его сложно, так как для понимания такого поведения нужно хорошо знать спецификацию.

Помимо упомянутых методов, простое добавление target="_blank" может иметь значительный эффект, поскольку многие браузеры уже устранили эту проблему.

В Chrome при нажатии на ссылку открывается новая вкладка с URL about:blank#blocked.

В Firefox открывается новая вкладка без URL.

В Safari ничего не происходит. Ни один из этих браузеров не выполняет JavaScript.

Тестируемые версии: Chrome 115, Firefox 116 и Safari 16.3.

В реальном мире большинство ссылок имеют атрибут target="_blank".

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

Для получения дополнительной информации вы можете обратиться к статьям: - The curious case of XSS and the mouse middle button - Anchor Tag XSS Exploitation in Firefox with Target="_blank"

Практический пример

Рассмотрим уязвимость, обнаруженную недавно (в июне 2023 года) в веб-версии Telegram. Она связана с псевдопротоколом javascript: и упоминается в статье Slonser История одной XSS в Telegram

В Telegram Web (у Telegram есть несколько веб-версий) есть функция ensureProtocol, которая проверяет наличие :// в URL. Если его нет, она автоматически добавляет http://:

export function ensureProtocol(url?: string) {
    if (!url) {
        return undefined;
    }
    return url.includes("://") ? url : `http://${url}`;
}

Эту защиту легко обойти, используя что-то вроде javascript:alert('://'). Это позволяет успешно использовать псевдопротокол javascript:. Однако проблема в том, что сервер также проверяет, является ли URL действительным адресом, а предыдущая строка явно не является таковой.

URL может включать имя пользователя и пароль в начале (используется для HTTP-аутентификации), разделенные двоеточием (`:`), например:

https://username:password@www.example.com

Поэтому Slonser обнаружил, что эту строку можно использовать для обхода проверки:

javascript:alert@github.com/#://

В этом случае javascript - это имя пользователя, alert - пароль, а хост - github.com. Хотя он не начинается с http:// или https://, сервер по-прежнему считает его допустимым URL.

Наконец, с помощью кодирования URL генерируется URL с паролем, который содержит только допустимые символы:

javascript:alert%28%27Slonser%20was%20here%21%27%29%3B%2F%2F@
github.com#;alert(10);://eow5kas78d0wlv0.m.pipedream.net%27//
 after decodedjavascript:alert('Slonser was here!');//@
github.com#;alert(10);://eow5kas78d0wlv0.m.pipedream.net'

Сервер распознает указанную выше строку как ссылку, и клиент может обойти проверку ://. Когда пользователь нажимает на эту ссылку, запускается XSS-атака.

Позже Telegram исправил эту проблему, реализовав метод, о котором я упоминал ранее, который проверяет URL-адрес и гарантирует, что протокол не является javascript:. Fix:

export function ensureProtocol(url?: string) {
    if (!url) {
        return undefined;
    }
    // HTTP was chosen by default as a fix for
    // https://bugs.telegram.org/c/10712
    // It is also the default protocol in the official TDesktop client.
    try {
        const parsedUrl = new URL(url);
        
        // eslint-disable-next-line no-script-url
        if (parsedUrl.protocol === "javascript:") {
            return `http://${url}`;
        }
        
        return url;
    } catch (err) {
        return `http://${url}`;
    }
}

Заключение

В этой статье мы рассмотрели опасные аспекты псевдопротокола javascript:. Его можно поместить внутри атрибута href тега <a>, что является распространенным вариантом использования. Кроме того, разработчики часто забывают о потенциальных рисках или могут не знать о них, что приводит к уязвимостям. Хотя в большинстве случаев гиперссылки открываются на новых вкладках, предотвращая выполнение кода JavaScript, нет никакой гарантии, что поведение будет таким везде (например, когда целевой атрибут не указан) или при использовании старых браузеров или альтернативных методов для открытия новых вкладок. Это создает опасность для пользователей. Кроме того, при выполнении перенаправлений важно учитывать проблемы, связанные с псевдопротоколом javascript:. Без надлежащей обработки данных - это может привести к XSS-уязвимости. Разработчикам крайне важно постоянно знать об этих проблемах и соответствующим образом решать их в коде, как сказано в знаменитой цитате:

Никогда не доверяйте пользовательскому вводу.

Last updated