Введение
Безопасность десктопных 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.