Различные статьи по Qt и c++

Безопасность Qt-приложений: защита от внедрения кода и проверка входных данных

Введение

Безопасность десктопных Qt-приложений часто недооценивается по сравнению с веб-разработкой, но риски реальны: приложения, обрабатывающие файлы из недоверенных источников, принимающие сетевые данные или выполняющие внешние команды, могут стать вектором атаки. В статье рассмотрим несколько ключевых направлений защиты — от инъекций команд до безопасной обработки пользовательского ввода — применительно к типичному Qt/C++ коду.

Концепция

Инъекция команд возникает, когда пользовательский ввод напрямую попадает в команду, передаваемую QProcess или системному shell, без должного экранирования — злоумышленник может внедрить дополнительные команды через специальные символы shell. Безопасное решение — передавать аргументы программе как отдельный список (QProcess::start(program, argumentsList)), а не как единую командную строку, формируемую конкатенацией строк, что устраняет саму возможность интерпретации пользовательских данных как части синтаксиса команды.

Аналогичный принцип применим к любому месту, где внешние данные могут быть интерпретированы как код или команды — SQL (рассмотренный отдельно через подготовленные запросы), пути к файлам (риск directory traversal через ../), регулярные выражения, построенные из пользовательского ввода (риск ReDoS — экспоненциального времени выполнения при специально сконструированном вводе).

Пример кода: Безопасность Qt-приложений

#include <QProcess>
#include <QDir>
#include <QFileInfo>
#include <QDebug>

// ОПАСНО: формирование командной строки конкатенацией позволяет инъекцию
void runCommandVulnerable(const QString &filename)
{
    QProcess process;
    // Если filename == "file.txt; rm -rf /", это будет интерпретировано shell'ом
    process.start("sh -c \"cat " + filename + "\"");
}

// БЕЗОПАСНО: аргументы передаются как отдельные элементы списка,
// без интерпретации через shell
void runCommandSafe(const QString &filename)
{
    QProcess process;
    process.start("cat", QStringList() << filename);
    process.waitForFinished();
}

// Защита от directory traversal при работе с пользовательскими путями
bool isPathSafe(const QString &basePath, const QString &userProvidedPath)
{
    const QDir baseDir(basePath);
    const QString canonicalBase = baseDir.canonicalPath();

    const QFileInfo requestedFile(baseDir, userProvidedPath);
    const QString canonicalRequested = requestedFile.canonicalFilePath();

    // Запрошенный путь должен оставаться внутри базовой директории
    // после разрешения всех ".." и симлинков
    return canonicalRequested.startsWith(canonicalBase + "/");
}

void demonstratePathValidation()
{
    const QString base = "/var/app/user_files";

    qDebug() << isPathSafe(base, "documents/report.pdf");     // true, безопасно
    qDebug() << isPathSafe(base, "../../etc/passwd");          // false, попытка выхода за пределы
}

Пояснения к коду

runCommandVulnerable показывает опасный паттерн: запуск через sh -c с конкатенированной строкой делает возможной инъекцию shell-команд через специальные символы (;, |, &&) во входных данных. runCommandSafe решает проблему принципиально: QProcess::start(program, arguments) с аргументами в виде QStringList передаёт их напрямую процессу через системный вызов exec без участия промежуточного shell — значение filename, даже если оно содержит символы shell-синтаксиса, передаётся программе cat как один буквальный аргумент, без какой-либо интерпретации.

isPathSafe защищает от directory traversal — попытки получить доступ к файлам за пределами разрешённой директории через относительные пути вида ../../etc/passwd. Ключевая идея — сравнивать не исходные строки путей, а их канонические формы (canonicalPath()/canonicalFilePath()), полученные после разрешения всех .. и символических ссылок, и проверять, что результат действительно остаётся внутри базовой директории.

Подводные камни

  • Использование QProcess::startCommand() или ручной конкатенации с последующим запуском через shell для удобства (чтобы не разбивать команду на аргументы вручную) — это удобство прямо противоречит безопасности, если хотя бы один компонент команды может содержать пользовательский ввод.
  • Проверка пути на отсутствие ".." в строке без получения канонической формы. Простая проверка if (!path.contains("..")) обходится тривиально через символические ссылки или иные способы кодирования пути (например, через закодированные в URL последовательности при сетевом вводе) — надёжная проверка требует именно сравнения канонических, полностью разрешённых путей.
  • Регулярные выражения, построенные напрямую из пользовательского ввода, без экранирования специальных символов regex (., *, (, ) и других) — может приводить либо к неожиданному поведению паттерна, либо, в случае сложных вложенных квантификаторов, к катастрофическому backtracking (ReDoS), способному «подвесить» приложение на одном специально сконструированном входе.
  • Доверие к данным, прошедшим через клиентскую (UI) валидацию, без повторной проверки на уровне обработки. Валидация в интерфейсе (например, ограничение длины текста в QLineEdit через setMaxLength) — мера удобства пользователя, а не безопасности; данные, обрабатываемые программно (включая чтение конфигурационных файлов, сетевые запросы), должны проверяться независимо от того, проходили ли они через UI.