- H-1 cert-DN spoofing: IRuntimeConfig::certAuthTrusted() now defaults to false (fail-closed). X-SSL-Client-DN is an ordinary request header; a loopback bind does not prove it came from a TLS-terminating proxy. Consumers must opt in explicitly behind a header-stripping proxy. - H-3 scope reparenting: ScopeGuardRepository::save() now also checks the EXISTING row's scope (via a new required entity-id accessor), so an actor can't claim an out-of-scope row by relabelling it in the request body. - H-2 IQueryable bypass: add ScopeGuardQueryable<T> — filters query() results through the same predicate so the queryable surface can't escape the scope guard. - H-4 TemporalRepository TOCTOU: serialise the read-modify-write with a per-instance mutex (no more duplicate-live / lost-update under concurrent same-entity saves) and add an optional TxRunner so the close-then-insert pair can commit/rollback atomically. - H-5 SMTP header injection: reject CR/LF/NUL in `to`/`fromAddress` before building the envelope and From:/To: header lines. Tests: expand test_repository_decorators (reparenting + queryable filtering), add curl-guarded test_smtp_transport (base64 vectors + CRLF guard). All 15 ctest targets pass. README updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
2.7 KiB
C++
75 lines
2.7 KiB
C++
// Tests for oatpp-authkit/mail/SmtpTransport.hpp.
|
|
//
|
|
// Covers the pure, network-free surface:
|
|
// - base64Encode against RFC 4648 vectors
|
|
// - hasHeaderInjectionChars
|
|
// - send() rejects CR/LF/NUL in recipient / from address BEFORE touching
|
|
// libcurl (the SMTP header-injection guard) — no live mail server needed,
|
|
// the validation short-circuits ahead of curl_easy_init / perform.
|
|
|
|
#include "oatpp-authkit/mail/SmtpTransport.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
int g_failures = 0;
|
|
|
|
#define REQUIRE(expr) do { \
|
|
if (!(expr)) { \
|
|
std::fprintf(stderr, "FAIL %s:%d %s\n", __FILE__, __LINE__, #expr); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
using namespace oatpp_authkit::mail;
|
|
|
|
void test_base64_rfc4648_vectors() {
|
|
REQUIRE(base64Encode("") == "");
|
|
REQUIRE(base64Encode("f") == "Zg==");
|
|
REQUIRE(base64Encode("fo") == "Zm8=");
|
|
REQUIRE(base64Encode("foo") == "Zm9v");
|
|
REQUIRE(base64Encode("foob") == "Zm9vYg==");
|
|
REQUIRE(base64Encode("fooba") == "Zm9vYmE=");
|
|
REQUIRE(base64Encode("foobar") == "Zm9vYmFy");
|
|
}
|
|
|
|
void test_header_injection_detector() {
|
|
REQUIRE(!hasHeaderInjectionChars("a@b.com"));
|
|
REQUIRE( hasHeaderInjectionChars("a@b.com\r\nBcc: evil@x.com"));
|
|
REQUIRE( hasHeaderInjectionChars("a@b.com\n"));
|
|
REQUIRE( hasHeaderInjectionChars("a@b.com\r"));
|
|
REQUIRE( hasHeaderInjectionChars(std::string("a@b.com\0x", 9))); // embedded NUL
|
|
}
|
|
|
|
void test_send_rejects_crlf_in_addresses() {
|
|
SmtpConfig cfg;
|
|
cfg.host = "localhost";
|
|
cfg.fromAddress = "noreply@example.com";
|
|
|
|
// CRLF in recipient → rejected with no network call.
|
|
std::string r1 = send("victim@example.com\r\nBcc: evil@x.com",
|
|
"subject", "<p>hi</p>", {}, cfg);
|
|
REQUIRE(r1.find("invalid recipient") != std::string::npos);
|
|
|
|
// CRLF in from address → rejected.
|
|
SmtpConfig cfg2 = cfg;
|
|
cfg2.fromAddress = "noreply@example.com\r\nSubject: spoofed";
|
|
std::string r2 = send("victim@example.com", "subject", "<p>hi</p>", {}, cfg2);
|
|
REQUIRE(r2.find("invalid from") != std::string::npos);
|
|
|
|
// Empty-config guards still fire (and come before the address checks).
|
|
SmtpConfig empty;
|
|
REQUIRE(send("a@b.com", "s", "b", {}, empty).find("no host") != std::string::npos);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
test_base64_rfc4648_vectors();
|
|
test_header_injection_detector();
|
|
test_send_rejects_crlf_in_addresses();
|
|
std::printf("%s (%d failures)\n", g_failures ? "FAIL" : "OK", g_failures);
|
|
return g_failures ? 1 : 0;
|
|
}
|