fold/tests/unit/test_taint_sanitizer.cpp

95 lines
3.6 KiB
C++

// Phase 1 (sanitizer recognition): a value escaped/validated for a domain must
// NOT trip a sink in that domain, but MUST still trip a sink in a different
// domain (no cross-domain masking).
#include <clang/Frontend/ASTUnit.h>
#include <clang/Tooling/Tooling.h>
#include <catch2/catch_test_macros.hpp>
#include <memory>
#include <string>
#include <vector>
#include "fold/detect/sql_injection.hpp"
#include "fold/detect/taint_track.hpp"
#include "fold/report/finding.hpp"
#include "fold/symbolic/budget.hpp"
namespace {
std::unique_ptr<clang::ASTUnit> parse(const std::string& code) {
return clang::tooling::buildASTFromCodeWithArgs(code, {"-std=c++17"}, "input.cpp");
}
std::size_t countKind(const std::vector<fold::Finding>& fs, fold::FindingKind k) {
std::size_t n = 0;
for (const auto& f : fs)
if (f.kind == k)
++n;
return n;
}
std::size_t runTaint(const std::string& code) {
auto unit = parse(code);
REQUIRE(unit);
fold::SolverBudget b;
fold::SolverStats s;
auto fs = fold::detectTaintTrackInContext(unit->getASTContext(), b, s);
return countKind(fs, fold::FindingKind::TaintTrack);
}
std::size_t runSql(const std::string& code) {
auto unit = parse(code);
REQUIRE(unit);
fold::SolverBudget b;
fold::SolverStats s;
auto fs = fold::detectSqlInjectionInContext(unit->getASTContext(), b, s);
return countKind(fs, fold::FindingKind::SqlInjection);
}
// Minimal prototypes so the fixtures parse as C++.
constexpr const char* kDecls =
"typedef struct MYSQL MYSQL; typedef struct PGconn PGconn;\n"
"extern \"C\" char* getenv(const char*);\n"
"extern \"C\" int system(const char*);\n"
"extern \"C\" int mysql_query(MYSQL*, const char*);\n"
"extern \"C\" unsigned long mysql_real_escape_string(MYSQL*, char*, const char*, unsigned "
"long);\n"
"extern \"C\" char* PQescapeLiteral(PGconn*, const char*, unsigned long);\n";
} // namespace
TEST_CASE("taint: unsanitized env input into system() fires", "[taint][sanitizer]") {
const std::string code =
std::string(kDecls) + "void f(){ char* u = getenv(\"X\"); system(u); }";
REQUIRE(runTaint(code) >= 1);
}
TEST_CASE("sql: unsanitized env input into mysql_query fires", "[taint][sanitizer]") {
const std::string code =
std::string(kDecls) + "void f(MYSQL* db){ char* u = getenv(\"X\"); mysql_query(db, u); }";
REQUIRE(runSql(code) >= 1);
}
TEST_CASE("sql: PQescapeLiteral output is not flagged", "[taint][sanitizer]") {
const std::string code = std::string(kDecls) +
"void f(MYSQL* db, PGconn* c){ char* u = getenv(\"X\");"
" char* q = PQescapeLiteral(c, u, 0); mysql_query(db, q); }";
REQUIRE(runSql(code) == 0);
REQUIRE(runTaint(code) == 0);
}
TEST_CASE("sql: mysql_real_escape_string destination is not flagged", "[taint][sanitizer]") {
const std::string code = std::string(kDecls) +
"void f(MYSQL* db){ char* u = getenv(\"X\"); char buf[256];"
" mysql_real_escape_string(db, buf, u, 255); mysql_query(db, buf); }";
REQUIRE(runSql(code) == 0);
}
TEST_CASE("cross-domain: SQL-escaped value into system() STILL fires", "[taint][sanitizer]") {
// PQescapeLiteral sanitizes for SQL, not for the shell — a command sink must
// still flag it. This guards against unsound cross-domain masking.
const std::string code = std::string(kDecls) +
"void f(PGconn* c){ char* u = getenv(\"X\");"
" char* q = PQescapeLiteral(c, u, 0); system(q); }";
REQUIRE(runTaint(code) >= 1);
}