oatpp-authkit/test/test_smtp_transport.cpp
Uwe Schuster 2e11408240 #16 (audit H-1..H-5): fix the five high-severity findings
- 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>
2026-05-29 12:49:03 +02:00

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;
}