/
smychkov
/
SStorage
Обзор
Документация
Войти
/
smychkov
/
SStorage
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/test_security.cpp
323 строки
12 KB
Андрей Смычков
security: комплексные фиксы по результатам аудита
25 апр 2026, 10:00
25 апр 2026, 10:00
5f1be8e
Код
Авторство
О чём код?
//============================================================================ // Тесты на безопасность: проверка защиты от malicious input //============================================================================ // Проверяем, что: // - Record::deserialize не падает на специально сформированных данных // - block.cpp отвергает overflow'ные заголовки // - Bloom::deserialize защищён от underflow // - WAL backpressure работает // - validateDataDirectory отвергает системные пути // - JSON escape корректно обрабатывает non-ASCII байты //============================================================================ #include "../src/core/record.hpp" #include "../src/sstable/block.hpp" #include "../src/sstable/bloom.hpp" #include "../src/util/utils.hpp" #include "../src/wal/wal.hpp" #include <atomic> #include <chrono> #include <cstdio> #include <cstdlib> #include <filesystem> #include <iostream> #include <string> #include <sys/stat.h> #include <thread> #include <unistd.h> #include <vector> using namespace sstorage; static int g_passed = 0; static int g_failed = 0; #define CHECK(cond) do { \ if (cond) { ++g_passed; } \ else { ++g_failed; std::cerr << "FAIL: " #cond " at line " << __LINE__ << "\n"; } \ } while (0) static std::string makeTempDir(const std::string& prefix) { std::string p = "/tmp/sstorage_sec_" + prefix + "_" + std::to_string(::getpid()) + "_" + std::to_string(rand()); ::mkdir(p.c_str(), 0755); return p; } static void cleanupDir(const std::string& dir) { std::error_code ec; std::filesystem::remove_all(dir, ec); } //============================================================================ // 1. Record::deserialize — защита от malicious varint //============================================================================ static void testRecordDeserializeAttacks() { size_t consumed; // 1.1. nullptr + len>0 → nullopt (не падаем) { auto r = Record::deserialize(nullptr, 100, consumed); CHECK(!r.has_value()); } // 1.2. Пустой буфер { auto r = Record::deserialize("", 0, consumed); CHECK(!r.has_value()); } // 1.3. varint с keyLen = 2^35 (атака из ревью) — buffer slightly meньше // Проверяем, что НЕ падаем и возвращаем nullopt. { std::string buf; // varint кодирующий очень большое число (длиной 5 байт) // 0xFF 0xFF 0xFF 0xFF 0x7F = 2^35 - 1 buf.push_back(static_cast<char>(0xFF)); buf.push_back(static_cast<char>(0xFF)); buf.push_back(static_cast<char>(0xFF)); buf.push_back(static_cast<char>(0xFF)); buf.push_back(static_cast<char>(0x7F)); auto r = Record::deserialize(buf.data(), buf.size(), consumed); CHECK(!r.has_value()); } // 1.4. Разумный keyLen, но больше оставшегося буфера { std::string buf; buf.push_back(100); // keyLen = 100 (1-byte varint) // но реальных байт нет auto r = Record::deserialize(buf.data(), buf.size(), consumed); CHECK(!r.has_value()); } // 1.5. Keylen больше лимита (64MB) — должны отвергнуть { std::string buf; // varint для 100 МБ = 100 * 1024 * 1024 = 104857600 // Кодируем как varint uint64_t bigKeyLen = 100ULL * 1024 * 1024; while (bigKeyLen >= 0x80) { buf.push_back(static_cast<char>((bigKeyLen & 0x7F) | 0x80)); bigKeyLen >>= 7; } buf.push_back(static_cast<char>(bigKeyLen)); auto r = Record::deserialize(buf.data(), buf.size(), consumed); CHECK(!r.has_value()); } // 1.6. Валидная запись остаётся валидной после всех ужесточений { Record orig("hello", "world", 42); std::string buf; orig.serialize(buf); auto r = Record::deserialize(buf.data(), buf.size(), consumed); CHECK(r.has_value()); CHECK(*r == orig); } } //============================================================================ // 2. block.cpp — защита от overflow'ного заголовка блока //============================================================================ static void testBlockHeaderAttacks() { std::string out; // 2.1. Пустой заголовок CHECK(!readBlockFromDisk("", 0, out)); // 2.2. Короче header'а { std::string buf(5, '\0'); CHECK(!readBlockFromDisk(buf.data(), buf.size(), out)); } // 2.3. Неизвестный тип компрессии (0xFF) { std::string buf(kBlockHeaderSize + 10, '\0'); buf[0] = static_cast<char>(0xFF); CHECK(!readBlockFromDisk(buf.data(), buf.size(), out)); } // 2.4. compressedSize = 0xFFFFFFFF → должен быть отвергнут как // превышающий лимит { std::string buf(kBlockHeaderSize + 100, '\0'); buf[0] = 0; // kNone // uncompressedSize = 10 buf[1] = 10; buf[2] = 0; buf[3] = 0; buf[4] = 0; // compressedSize = 0xFFFFFFFF buf[5] = static_cast<char>(0xFF); buf[6] = static_cast<char>(0xFF); buf[7] = static_cast<char>(0xFF); buf[8] = static_cast<char>(0xFF); CHECK(!readBlockFromDisk(buf.data(), buf.size(), out)); } // 2.5. uncompressedSize = 100 МБ (выше лимита 64 МБ) { std::string buf(kBlockHeaderSize + 10, '\0'); buf[0] = 0; uint32_t big = 100u * 1024u * 1024u; buf[1] = static_cast<char>(big & 0xFF); buf[2] = static_cast<char>((big >> 8) & 0xFF); buf[3] = static_cast<char>((big >> 16) & 0xFF); buf[4] = static_cast<char>((big >> 24) & 0xFF); CHECK(!readBlockFromDisk(buf.data(), buf.size(), out)); } // 2.6. parseBlockRecords: numRecords > оставшегося буфера { std::vector<Record> records; std::string buf; // varint numRecords = 1000000 (4 байта varint) uint64_t v = 1000000; while (v >= 0x80) { buf.push_back(static_cast<char>((v & 0x7F) | 0x80)); v >>= 7; } buf.push_back(static_cast<char>(v)); // Дальше буфер пустой — явно не 1M записей влезет CHECK(!parseBlockRecords(buf.data(), buf.size(), records)); } } //============================================================================ // 3. Bloom::deserialize — защита от underflow //============================================================================ static void testBloomDeserializeAttacks() { BloomFilter bf; size_t consumed; // 3.1. Пустой буфер CHECK(!bf.deserialize("", 0, consumed)); // 3.2. numHashes = 0 (запрещено) { std::string buf; buf.push_back(0); CHECK(!bf.deserialize(buf.data(), buf.size(), consumed)); } // 3.3. numHashes = 100 (выше разумного) { std::string buf; uint64_t n = 100; while (n >= 0x80) { buf.push_back(static_cast<char>((n & 0x7F) | 0x80)); n >>= 7; } buf.push_back(static_cast<char>(n)); CHECK(!bf.deserialize(buf.data(), buf.size(), consumed)); } // 3.4. bitsBytes = 200 МБ — выше лимита 128 МБ { std::string buf; buf.push_back(7); // numHashes = 7 (1-byte varint) uint64_t big = 200ULL * 1024 * 1024; while (big >= 0x80) { buf.push_back(static_cast<char>((big & 0x7F) | 0x80)); big >>= 7; } buf.push_back(static_cast<char>(big)); CHECK(!bf.deserialize(buf.data(), buf.size(), consumed)); } } //============================================================================ // 4. WAL backpressure — очередь не растёт неограниченно //============================================================================ static void testWalBackpressure() { auto dir = makeTempDir("wal_backpressure"); // Маленький лимит: 64 КБ — легко упереться WriteAheadLog wal(dir, 1, 64 * 1024); CHECK(wal.open()); // Пишем 200 записей по 1 КБ. В сумме 200 КБ, но лимит очереди 64 КБ. // Без backpressure был бы OOM; с backpressure — producer будет ждать // пока writer thread освободит место. std::atomic<int> completed{0}; auto start = std::chrono::steady_clock::now(); for (int i = 0; i < 200; ++i) { CHECK(wal.append(Record("k" + std::to_string(i), std::string(1024, 'x'), i))); ++completed; } auto dur = std::chrono::steady_clock::now() - start; // Все записи должны быть успешно записаны (не потеряны) CHECK(completed.load() == 200); wal.close(); // Проверяем, что все записи реально сохранены auto records = WriteAheadLog::replay(WriteAheadLog::buildFilename(dir, 1)); CHECK(records.size() == 200); // Время работы должно быть разумным (защита от зависания) CHECK(dur < std::chrono::seconds(10)); cleanupDir(dir); } //============================================================================ // 5. validateDataDirectory — защита от path traversal и системных путей //============================================================================ static void testValidateDataDirectory() { std::string resolved; // 5.1. Пустой путь — запрещён CHECK(!util::validateDataDirectory("", resolved)); // 5.2. NULL-байт внутри { std::string evil("/tmp/\0/data", 11); CHECK(!util::validateDataDirectory(evil, resolved)); } // 5.3. Управляющий символ CHECK(!util::validateDataDirectory("/tmp/\x01/data", resolved)); // 5.4. Системные пути — запрещены CHECK(!util::validateDataDirectory("/etc", resolved)); CHECK(!util::validateDataDirectory("/etc/passwd", resolved)); CHECK(!util::validateDataDirectory("/proc", resolved)); CHECK(!util::validateDataDirectory("/sys/kernel", resolved)); CHECK(!util::validateDataDirectory("/dev", resolved)); CHECK(!util::validateDataDirectory("/bin", resolved)); CHECK(!util::validateDataDirectory("/sbin", resolved)); CHECK(!util::validateDataDirectory("/usr/bin", resolved)); // 5.5. Допустимые пути { auto dir = makeTempDir("valid"); CHECK(util::validateDataDirectory(dir, resolved)); // resolved должен быть absolute CHECK(!resolved.empty()); CHECK(resolved[0] == '/'); cleanupDir(dir); } // 5.6. ./local/path разрешён CHECK(util::validateDataDirectory("./mydata", resolved)); // 5.7. Относительный без точки — нормализуется до ./ CHECK(util::validateDataDirectory("mydata", resolved)); } //============================================================================ // main //============================================================================ int main() { testRecordDeserializeAttacks(); testBlockHeaderAttacks(); testBloomDeserializeAttacks(); testWalBackpressure(); testValidateDataDirectory(); std::cout << "test_security: passed=" << g_passed << " failed=" << g_failed << "\n"; return g_failed == 0 ? 0 : 1; }