95 lines
3.6 KiB
C++
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);
|
|
}
|