ИСП РАН
Содержание

Фаззинг-тестирование noVNC

Введение

Современные облачные платформы и системы виртуализации повсеместно используют web-консоли для удалённого доступа к виртуальным машинам. В основе большинства таких решений лежит noVNC — JavaScript-реализация VNC-клиента, работающая прямо в браузере. Эта библиотека используется в:

  • OpenStack Horizon — консоль виртуальных машин.
  • Proxmox VE — web-интерфейс управления виртуализацией.
  • oVirt/RHEV — корпоративные решения Red Hat.
  • Kubernetes Dashboard — через VNC-proxy к подам.
  • Множество коммерческих облаков — AWS WorkSpaces, Google Cloud и др.

С точки зрения безопасности, noVNC представляет особый интерес по нескольким причинам:

  1. Обработка недоверенных данных — клиент получает поток данных от VNC-сервера, который может быть скомпрометирован или специально модифицирован атакующим.
  2. Сложные бинарные протоколы — декодирование изображений (Raw, Hextile, Tight, ZRLE), сжатие zlib, криптографические операции.
  3. Выполнение в браузере — уязвимости могут привести к XSS, утечке данных или отказу в обслуживании.
  4. Обработка буфера обмена — Extended Clipboard Protocol позволяет передавать произвольные данные между клиентом и сервером.

В данной статье мы подробно рассмотрим, как провести фаззинг c обратной связью по покрытию (coverage guided) критических модулей noVNC с использованием современных инструментов, какие технические проблемы возникают при тестировании JavaScript-кода и как их преодолеть.

1. Обоснование выбора Jazzer.js

Для фаззинг-тестирования JavaScript-кода существует несколько подходов:

Инструмент Тип Особенности
Jazzer.js Coverage-guided Основан на libFuzzer, инструментирует код
Jsfuzz Coverage-guided Устаревший, не поддерживается
AFL.js Coverage-guided Экспериментальный

Мы выбрали Jazzer.js — это порт легендарного Jazzer (Google) для экосистемы JavaScript/Node.js. Ключевые преимущества:

  • Coverage-guided мутации — фаззер отслеживает, какие ветви кода исполняются, и генерирует входные данные, максимизирующие покрытие.
  • Интеграция с libFuzzer — используется проверенный движок мутаций.
  • Поддержка корпуса — сохранение интересных тест-кейсов между запусками.
  • Детекция ошибок — автоматически находит аварийные завершения, зависания, OOM.

2. Проблема инструментирования ESM модулей

Суть проблемы

noVNC написан с использованием ES Modules (ESM) — современного стандарта модульной системы JavaScript:

// core/decoders/raw.js
import { toSigned32bit } from '../util/int.js';
export default class RawDecoder { ... }

Однако Jazzer.js инструментирует код путём перехвата require() — механизма CommonJS (CJS). При попытке импортировать ESM модули инструментация просто не происходит, и фаззер работает "вслепую", без обратной связи по покрытию.

Дополнительная сложность - top-level await

Модуль core/util/browser.js в noVNC использует top-level await для определения возможностей браузера:

// core/util/browser.js
const _supportsCursorURIs = await (async function() {
    // Проверка поддержки cursor URIs в браузере
})();
export { _supportsCursorURIs as supportsCursorURIs };

При попытке импортировать такой модуль в Node.js без браузерного окружения возникает ошибка. Даже если бы мы могли загрузить ESM напрямую, код упал бы на отсутствии объектов window, document, navigator.

Решение - транспиляция через Rollup

Мы применили многоступенчатый подход:

  • Сборка ESM → CommonJS с помощью Rollup.
  • Подмена проблемных модулей через plugin.
  • Внедрение browser mock в каждый бандл.

Конфигурация Rollup (rollup.config.js):

const resolve = require('@rollup/plugin-node-resolve').default;
const commonjs = require('@rollup/plugin-commonjs');
const path = require('path');

// Plugin для замены browser.js на mock
const replaceBrowserModule = {
  name: 'replace-browser-module',
  resolveId(source, importer) {
    if (source.endsWith('util/browser.js')) {
      return path.resolve(__dirname, 'browser-mock-module.js');
    }
    return null;
  }
};

// Browser mock, внедряемый в начало каждого бандла
const browserMockBanner = <code>
global.window = global.window || {
    location: { protocol: 'http:', hostname: 'localhost' },
    performance: { now: () => Date.now() },
    screen: { width: 1920, height: 1080 }
};
global.document = global.document || {
    createElement: () => ({
        getContext: () => ({
            createImageData: (w, h) => ({ 
                data: new Uint8ClampedArray(w * h * 4), width: w, height: h 
            }),
            putImageData: () => {}, drawImage: () => {}
        })
    })
};
global.navigator = { userAgent: 'Mozilla/5.0', platform: 'Linux' };
`;

module.exports = [
  {
    input: '../core/decoders/raw.js',
    output: { file: 'bundled/raw-decoder.cjs', format: 'cjs', banner: browserMockBanner },
    plugins: [resolve(), commonjs()]
  },
  // ... аналогично для других модулей
];

В результате получаем набор .cjs файлов, которые:

  • Корректно загружаются через require().
  • Инструментируются Jazzer.js.
  • Не падают из-за отсутствия браузерного окружения.

3. Выбор фаззинг-целей

3.1 Тестирование декодеров изображений

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

Кодировка Описание Риски
Raw Несжатые пиксели Buffer overflow при неверных размерах
Hextile Тайловое кодирование Переполнение при рекурсивном разборе
Tight zlib + JPEG Уязвимости декомпрессии, JPEG parsing
ZRLE zlib + RLE Integer overflow в длинах

Структура фаззинг-харнесса (fuzz-decoders.js):

const RawDecoder = require('./bundled/raw-decoder.cjs');
const HextileDecoder = require('./bundled/hextile-decoder.cjs');
const TightDecoder = require('./bundled/tight-decoder.cjs');
const ZRLEDecoder = require('./bundled/zrle-decoder.cjs');

module.exports.fuzz = function(data) {
    if (data.length < 20) return; // Минимальный размер для осмысленного теста
    
    const bytes = new Uint8Array(data);
    
    // Выбор декодера и параметров на основе входных данных
    const decoderType = bytes[0] % 4;
    const width = ((bytes[3] << 8 | bytes[4]) % 800) + 1;
    const height = ((bytes[5] << 8 | bytes[6]) % 600) + 1;
    const depth = (bytes[11] % 3 === 0) ? 8 : 24;
    
    // Создание mock-объектов для эмуляции среды выполнения
    const mockDisplay = {
        _fbWidth: 1920, _fbHeight: 1080,
        blitImage: (x, y, w, h, data, offset) => { /* эмуляция отрисовки */ },
        fillRect: (x, y, w, h, color) => { /* заливка цветом */ }
    };
    
    const mockSock = createMockWebSocket(bytes.slice(12));
    
    // Выбор и запуск декодера
    const decoders = [new RawDecoder(), new HextileDecoder(), 
                      new TightDecoder(), new ZRLEDecoder()];
    const decoder = decoders[decoderType];
    
    decoder.decodeRect(x, y, width, height, mockSock, mockDisplay, depth);
};

Ключевая особенность — мы эмулируем всё окружение: WebSocket для чтения данных, Display для отрисовки. Это позволяет тестировать декодеры изолированно, без реального сетевого соединения.

3.2 Clipboard Fuzzer: Extended Clipboard Protocol

VNC Extended Clipboard — расширение протокола для обмена данными буфера обмена. Он поддерживает:

  • Текст (plain text, UTF-8).
  • RTF (форматированный текст).
  • HTML.
  • DIB (Device Independent Bitmap).
  • Файлы.

Протокол использует zlib-сжатие, что создаёт дополнительную поверхность атаки:

const Inflator = require('./bundled/inflator.cjs');
const Deflator = require('./bundled/deflator.cjs');
const { encodeUTF8, decodeUTF8 } = require('./bundled/strings-util.cjs');

module.exports.fuzz = function(data) {
    const bytes = new Uint8Array(data);
    
    // Парсинг флагов протокола (4 байта)
    const actions = bytes[0] << 24;  // Caps, Request, Peek, Notify, Provide
    const formats = bytes[3];         // Text, RTF, HTML, DIB, Files
    
    const isProvide = !!(actions & extendedClipboardActionProvide);
    
    if (isProvide && (formats & extendedClipboardFormatText)) {
        // Распаковка zlib-сжатых данных
        const streamInflator = new Inflator();
        streamInflator.setInput(bytes.slice(4));
        
        // Чтение размера и данных для каждого формата
        const sizeArray = streamInflator.inflate(4);
        const size = (sizeArray[0] << 24) | (sizeArray[1] << 16) | 
                    (sizeArray[2] << 8) | sizeArray[3];
        
        const chunk = streamInflator.inflate(Math.min(size, 1024 * 1024));
        
        // Декодирование UTF-8 и нормализация
        let textData = decodeUTF8(chunk);
        textData = textData.replaceAll("\r\n", "\n");
    }
};

Можно модифицировать фаззинг-цель так, чтобы заголовок файла (magic bytes, сигнатуры JPEG/ZIP) оставался константным и не подвергался мутациям. Это предотвращает генерацию невалидных файлов, отбрасываемых на первой же проверке формата.

4. Запуск фаззинг-тестирования

4.1 Сборка инструментированных модулей

cd ~/noVNC/fuzz

# Установка зависимостей
npm install

# Сборка CommonJS бандлов
npm run build

Результат — 7 файлов в каталоге bundled/:

  • raw-decoder.cjs, hextile-decoder.cjs, tight-decoder.cjs, zrle-decoder.cjs.
  • inflator.cjs, deflator.cjs.
  • strings-util.cjs.

4.2 Создание начального корпуса

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

  • Валидный Raw-прямоугольник 16x16.
  • Hextile с различными типами тайлов.
  • Tight с JPEG-сжатием.

4.3 Запуск фаззеров

Decoders Fuzzer:
npx jazzer fuzz-decoders-cjs.js corpus -i bundled/ \
    -- -max_total_time=7200 --sync -print_final_stats=1

Фаззинг Decoders

Clipboard Fuzzer:
npx jazzer fuzz-clipboard-cjs.js corpus-clipboard -i bundled/ \
    -- -max_total_time=7200 --sync -print_final_stats=1

Фаззинг Clipboard

5. Сбор и анализ покрытия кода

5.1 Методология

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

Мы используем c8 — инструмент coverage для Node.js, основанный на V8 coverage API:

# Прогон всех тест-кейсов из корпуса с измерением покрытия
npm run coverage:collect

Coverage CLI

Скрипт collect-coverage.js последовательно загружает каждый файл из корпуса и прогоняет через соответствующий фаззер:

function runCorpusFile(fuzzerModule, inputFile) {
    const data = fs.readFileSync(inputFile);
    const fuzzer = require(fuzzerModule);
    fuzzer.fuzz(data);
}

// Обработка всех файлов корпуса
for (const file of corpusFiles) {
    runCorpusFile('./fuzz-decoders.js', file);
}

5.2 Генерация отчётов

npm run coverage:report

6. Результаты фаззинг-тестирования

6.1 Статистика выполнения

Фаззер Запусков Найдено путей Время
Decoders > 800 000 1350 2 часа
Clipboard > 400 000 538 2 часа

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

Разница в количестве путей объясняется сложностью кода:

  • Декодеры содержат множество ветвлений для разных типов данных.
  • Clipboard имеет среднюю сложность (сжатие + парсинг).

6.2 Покрытие кода

Coverage All

Coverage Modules

7. Выводы

В данной статье представлена методика проведения coverage-guided фаззинг-тестирования JavaScript-библиотек на примере noVNC. Ключевые элементы методики:

  1. Транспиляция ESM → CommonJS — использование Rollup для преобразования ES-модулей в формат, поддерживающий инструментирование Jazzer.js.
  2. Эмуляция браузерного окружения — создание mock-объектов для window, document, navigator, позволяющих выполнять browser-only код в среде Node.js.
  3. Изолированное тестирование модулей — построение фаззинг-целей с эмуляцией WebSocket и Display для тестирования декодеров без сетевого соединения.
  4. Сбор покрытия — использование c8 для измерения code coverage и оценки полноты тестирования.

Представленный подход применим к широкому классу JavaScript-библиотек, обрабатывающих недоверенные данные, и может быть адаптирован для тестирования других компонентов. Методика легко интегрируется в CI/CD pipeline для регулярного фаззинг-тестирования.

Автор:
Козачок Александр Васильевич, доктор технических наук доцент, заведующий лабораторией 12.2 ИСП РАН
Николаев Дмитрий Александрович, кандидат технических наук, научный сотрудник лаборатории 12.2 ИСП РАН

На нашем сайте мы используем cookie файлы, содержащие информацию о предыдущих посещениях веб-сайта. Данные обрабатываются для улучшения качества работы нашего веб-сайта. Если вы не хотите использовать cookie файлы, измените настройки браузера.