Code Audit Report — 2026-05-29 #16
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Code Audit Report — 2026-05-29
Automated audit of oatpp-authkit covering 23 audit categories (12 security + 11 code quality), run via the
/code-auditskill. Each category was executed by an independent subagent reading the live source underinclude/oatpp-authkit/**.Framing: oatpp-authkit is a reusable header-only C++ auth/security library consumed by webapps (e.g. fewo-webapp). It has no frontend, no controllers/DTOs of its own, and stores no PII itself. The dominant risk theme is insecure-by-default contracts: a fail-open default in a security library has fleet-wide blast radius across every consumer that inherits it. Frontend-specific audits (and audit 23/Orval) are N/A.
Executive Summary
Overall posture: MEDIUM, trending HIGH-risk-by-default for naive consumers. The library has genuinely strong defaults (strict CSP/HSTS/nosniff/frame headers, body-size + WS connection caps, leak-free error handler, no secret logging, fail-closed encryption-key gate, fully parameterized SQL). Its highest-impact weaknesses are fail-open trust boundaries and incomplete authorization decorators that silently shift risk onto consumers. No Critical in the default posture; the worst trust-boundary flaw (cert-DN spoofing) becomes effectively critical under the standard loopback-behind-proxy misdeployment.
Unique security findings by severity (deduplicated):
Immediate actions (top 3):
certAuthTrusted()→ false, WS emptypropertyIds→ no access, validateRateLimiterctor.IQueryable::query(), and makesave()check the existing row's scope, not just the incoming DTO.to/fromAddress.Security Findings
1. Initial Security Analysis — risk 5.5/10
X-Requested-With; cert path has no CSRF gate.requireUserstd::stoion bundle id (auth/RequireRole.hpp:35) can throw → 500 instead of 401;AuthPrincipal::idisintwhile the user store migrated to TEXT/UUID.AuditLog::escapeJson(db/AuditLog.hpp:89-98) escapes only\/".2. Session & Cookie Security — risk 4.5/10
find("session=")(util/TokenExtract.hpp:23-28) — matchesxsession=/notsession=/etc., takes first match → token confusion, defeats__Host-prefix guarantees. Fix: tokenize on;, exact-name match.setSessionCookiehelper (Secure/HttpOnly/SameSite); consumers hand-rollSet-Cookieand the baseline doc only recommendsSecure.X-Requested-Withcheck, no Origin/Referer validation, broken if consumer enables permissive CORS.X-SSL-Client-DNcert trust defaults toisLoopback()(see synthesis H-1).3. Authentication Flow — risk 5.5/10
RateLimiterexists but is NOT wired intoAuthInterceptor— entirely consumer responsibility, undocumented.AuthInterceptor.hpp:226-231) fails open: any request → admin whensetupModeActive() && !hasActiveUsers(); fail-open ifhasActiveUsers()swallows a DB error tofalse; no loopback requirement.TokenHasheris consumer-supplied with no enforced minimum strength; no constant-time compare helper shipped.IAuthBackend/IAuthPolicy/IRuntimeConfigstrategy split is excellent; library never sees plaintext passwords; deny-by-default at the interceptor.4. Input Validation — risk 5/10
toandfromAddressconcatenated into headers + envelope with no CRLF/NUL validation (mail/SmtpTransport.hpp:84,87,132-134). Subject is base64-safe; to/from are not. A security library must self-defend.repo/IQueryable.hpp:221-224) — wildcard metachars pass through (no SQLi, values bound; full-scan/match-semantics risk).BodySizeLimitInterceptorContent-Lengthparse accepts+/leading-whitespace forms (theoretical parser-desync).5. Authorization — risk 6.5/10 (highest)
IQueryable::query()bypass: the guard decorates only the 4Repository<T>methods;IQueryable::query()(repo/IQueryable.hpp:314-319) is a separate surface it does NOT wrap → arbitrary filtered SELECTs bypass scope entirely (full cross-tenant read).save()checks only the incoming DTO, never the existing row: actor scoped to prop-A submits an update for an entity currently in prop-B withpropertyId=prop-Ain the body → overwrites/reparents the prop-B row. Fix: load existing row, require predicate on BOTH.propertyIds= "all access" (ws/Hub.hpp:159-163); (b) cert trust loopback default; (c)BodySizeLimit requireContentLength=falseescape hatch.requireAdminis a proper gate.6. Database Security — risk 3.5/10
repo/TemporalRepository.hpp:196-207): two separate saves, no transaction → crash/error leaves duplicate-live or lost-version. Cascade soft-deletes likewise.RedactedFieldRepositoryfails open if decorators misordered: redaction fires only when stacked between Temporal and concrete; wrong order silently retainspassword_hashin history. Field matched by name only — silent if typo'd.7. Secrets Management — risk 1.5/10
AuditLog::logUpdate<Dto>can persistpassword_hash/tls_cert_dnintochanged_fields(not inSKIP_FIELDS).--allow-plaintextis dev-only and logs only the env-var name.8. Business / Stateful Logic — risk 5.5/10
TemporalRepository::saveTOCTOU: read-modify-write across 3 inner calls, no lock/transaction → concurrent same-entity saves cause lost update or two SENTINEL (live) rows. (Same root as #6 atomicity.)RateLimiterctor unvalidated:refillRate<=0evicts every bucket each sweep → limiter silently disabled (brute-force bypass);NaN→ DoS. Fix: validatecapacity>=1,refillRate>0, finite.thread_local t_pendingAuthhandoff: ifonAfterCreateruns on another thread / isn't reset on a failure path, stale auth could attach to another socket. Fails closed when empty.mt19937_64; predictable if consumers exposeentity_idas unguessable handles.9. API & Infrastructure — risk 4/10
"unknown"bucket when not loopback-bound / XFF absent → shared-bucket DoS + per-IP brute-force throttle stops working.CURLUSESSL_TRYopportunistic TLS — STARTTLS-strip sends credentials/mail cleartext; verify disabled for localhost. Fix:CURLUSESSL_ALLfor non-loopback.JsonErrorHandlerleaks no stack traces. No CORS shipped (same-origin by design) — but no guardrail against the*-with-credentials mistake.10. Logging & Monitoring — risk 4/10
OATPP_LOGWinlogEvent(auth/AuthInterceptor.hpp:178-182) — CR/LF/control chars in the request target forge log lines.LOGD(ws/Hub.hpp:203).AuditLog::escapeJsonincomplete → invalid/injectable JSON stored inaudit_log.changed_fields.11. File Handling — risk 0.5/10
NOTIFY_SOCKETpath (systemd/Notify.hpp), sourced from the trusted init environment, correctly bounded (no overflow), datagram target (no file primitive). Minor: over-long path silently truncated.12. Compliance Synthesis (OWASP / GDPR / PCI)
password_hashin history) — treat as the GDPR-adjacent priority. PlusAuditLogcan persist credential hashes intochanged_fields.Code Quality Findings
13. Architecture & Design — modularity 6/10
ws/Hub.hpp(438 lines) is a god header riddled with consumer-domain vocabulary (notifyBooking/notifyPerson/booking_id/propertyIds), contradicting the library's stated portability contract. An all-static singleton fusing registry + broadcast + presence + JSON + a static-init detached thread.audit_logtable designs ship in one library:db/AuditLog.hpp(table_name/operation/changed_fields/actor/created_at) vsrepo/AuditLogRepository.hpp(actor_user_id/entity_type/op/timestamp_ms). A consumer wiring both gets conflictingCREATE TABLE audit_log— latent runtime collision.AuthPrincipal::idisintbut the repo/db layer migrated user IDs to TEXT/UUID — latent contract break (and thestd::stoithrow).repo/claims ORM-agnosticism butdb/*+Concrete*hard-couple to oatpp-sqlite, with no CMake component split.IAuthBackend/IAuthPolicy/IRuntimeConfigseams and theRepository<T>decorator stack are textbook dependency-inversion + decorator.14. Code Duplication
Concrete*Repositoryadapters (~150 LOC of identical fetch-into-vector boilerplate). Extract aFunctionRepository/CRTP base.valid_from <= datetime('now') AND valid_until > datetime('now')repeated 14×; soft-delete SQL 5×; upsertON CONFLICTblocks per table.SENTINELtimestamp literal9999-12-31T23:59:59Zre-typed in 5 places (drift risk vs the runtime comparison).PolymorphicDispatcherreflection walk copy-pasted across 3 decorators (cloneDto,redactFields,logUpdate) — fragile coupling to oatpp internals.15. Error Handling
save/softDelete) discard the DB result — a failedINSERT ... ON CONFLICT/UPDATEis silently swallowed; combined with the non-transactionalTemporalRepository::save, a mid-sequence failure leaves a half-applied version with no rollback, and the audit decorator records "success."requireUserstd::stoican throw → generic 500 instead of clean 401.send()leaksCURL*/curl_slist/curl_mimehandles on any throw (e.g.std::bad_allocduring header assembly) — no RAII guard.CURLUSESSL_TRY).ScopeDeniedException/SchemaContractViolationhave no in-library HTTP-status mapping — an uncaughtScopeDeniedExceptionbecomes a 500 (authz denial leaks as server error). Ship a translation helper.16. Exception Flow
AuthInterceptor::interceptis uncaught — a transient DB blip during auth yields an unlogged, uncategorized error surface (and thehasActiveUsers()-swallowed-to-false path can fail open to setup-mode admin).TemporalRepository::saveis misinterpreted as "fresh insert" → duplicate live row.TemporalRepository::savetwo-write sequence with no transaction (same as #6/#8).AuditLogRepository::emitcatch(...)swallows non-std::exceptionsink failures silently (no log) → undetectable audit gaps.17. Code Quality Metrics
ws/Hub.hppgod header (438 lines, 5 responsibilities, static-init thread).audit_logschema.repo/IQueryable.hpp(323 lines) packs the whole SQL-AST DSL in one header.TemporalRepository::savemixes abstraction levels;findByEntityId/list/historyeach re-implement the same O(n) full-list()scan (4 duplicated scans; per-write full-table fetch).AuthInterceptor::intercept~80-line, ~12-complexity decision gauntlet with an embedded function-static session-sweep timer.SchemaContract.hppfan-in=9 → rebuild hot-spot. Otherwise well-sized: only 2 headers strictly >300 lines.18. Design Patterns
IQueryableis not forwarded by any decorator — wrapping anIQueryablerepo in the stack loses the queryable surface, and a downcast to the inner bypassesScopeGuardRepository(authz bypass; ties to security H-1).RedactedFieldRepositoryinfers "is historical" fromvalid_until, working only when stacked belowTemporalRepository; wrong order silently disables redaction with no error.intercept()via a function-local static (stateful at process scope, untestable, on the hot path).19. SOLID — rating 7/10
db::AuditLogdepends on concreteoatpp::orm::Executorinstead of implementing the existingIAuditSink).ws/Hub.hppgod object with fewo-domain notifications in a reusable header.RedactedFieldRepositorynot substitutable as a bareRepository<T>— redaction only correct when stacked under Temporal.IAuthBackendcould give defaults to more methods so an API-key-only consumer needn't stub session/maintenance methods.SchemaContract::verifyhardcodes SQLitepragma_table_infoprobe SQL despite claiming engine portability — inject anISchemaIntrospector.AuditLog::valueToJsonhardcodes a type ladder; unknown types silently serialize asnull.20. Testing
intercept()) is entirely untested — public-path bypass, setup-mode grant, cert-DN trust gate, session-vs-apikey resolution, CSRF, readonly-mutation block all unverified. Every authorization boundary in the library is untested.clientIpTrusted/ cookie + bearer token extraction untested (thexsession=substring trap may reveal a real bug).RateLimiteruntested (refill, eviction, concurrency, ctor validation) — needs an injectable clock.socketHasPropertyAccessauthz matrix, SMTPbase64Encode+ CRLF guard untested.makeReqHTTP fixture as the keystone, and standardizing theabort()-vs-counter harness idiom.21. Readability & Naming
audit_logtable shapes under the same name with divergent column vocabulary (table_namevsentity_type,operationvsop,actorvsactor_user_id).repo/IQueryable.hppis the only file using trailing-underscore member naming (col_,where_, …) vs the library-widem_prefix.BodySizeLimitInterceptor_hpp/SecurityHeadersInterceptor_hpp/UTIL_RATE_LIMITER_HPPare un-namespaced and collision-prone for a header-only lib.dto/InternalDto.hppuses snake_case C++ identifiers (booking_id) mixing with camelCase in the same struct — also leaks consumer domain.22. Resilience & Fault Tolerance — rating 6/10
IAuthBackenderror contract — a swallowedhasActiveUsers()error can grant setup-mode admin. Document fail-closed contract / add aBackendUnavailable→503 path.thread_localhandoff fragility; function-static session-sweep timer runs on the request hot path and only on traffic.TemporalRepository::savenon-atomic multi-step (consumer-delegated, but the library should document the transaction requirement or wrap it).s_mxacross all sends (head-of-line blocking) + a raw-pointer UAF window vs socket teardown.23. Orval Codegen Duplication
frontend/, noopenapi.json, no Orval config, no React/TypeScript, no generated client. This audit category does not apply to a backend C++ library.Prioritized Remediation Plan
certAuthTrusted()to false; require explicit opt-in + document that the proxy must strip inboundX-SSL-Client-DN—auth/IRuntimeConfig.hpp:47-49,auth/AuthInterceptor.hpp:236-246IQueryable::query()bypass — provide a scope-aware queryable wrapper or refuse to expose it —repo/ScopeGuardRepository.hpp,repo/IQueryable.hpp:314-319ScopeGuardRepository::save()load the existing row and require the scope predicate on BOTH existing and incoming DTO —repo/ScopeGuardRepository.hpp:94-99TemporalRepository::saveatomic (transaction or per-entity lock) and check write results so a failed read isn't misread as "fresh insert" —repo/TemporalRepository.hpp:180-207,repo/ConcreteUserRepository.hpp:53-59to/fromAddressbefore building headers/envelope —mail/SmtpTransport.hpp:84,87,132-134propertyIdsfrom "all access" to "no access" —ws/Hub.hpp:159-163RateLimiterctor (capacity>=1,refillRate>0, finite) so it cannot silently disable —util/RateLimiter.hpp:31-32hasActiveUsers()errors as "users exist" (fail closed), synchronize the check —auth/AuthInterceptor.hpp:226-231find("session=")with exact-name;-tokenized parsing —util/TokenExtract.hpp:23-28ws/Hub.hpp,ws/Listener.hppAuthInterceptor::interceptin try/catch → 503; document theIAuthBackendfail-closed contract —auth/AuthInterceptor.hpp:203-283auth/AuthInterceptor.hpp:178-182audit_logdesigns (one schema; makedb::AuditLoganIAuditSinkimpl) and completeescapeJsoncontrol-char escaping + add credential fields toSKIP_FIELDS—db/AuditLog.hpp,repo/AuditLogRepository.hpptest_auth_interceptor.cppcovering the full decision matrix (the single highest-value test gap), plusclientIpTrusted/extractTokenand behavioral body-size/security-header testsws/Hub.hpp, lift fewo-domainnotifyBooking/notifyPerson/booking_idout of the library, and give the housekeeper thread a shutdown pathFindings cite exact file:line. Items marked "Unable to verify" require the consumer app (e.g. concrete
IAuthBackendDB-error behavior, runtime proxy/TLS config). The recurring theme is insecure-by-default contracts in a library whose defaults propagate to every consumer — flipping the fail-open defaults to fail-closed is the highest-leverage hardening, protecting the entire downstream fleet rather than one app.The 5 high-severity findings are fixed in commit
2e11408onmain:IRuntimeConfig::certAuthTrusted()now defaults tofalse(fail-closed); explicit opt-in required.ScopeGuardQueryable<T>which filtersquery()results through the scope predicate.ScopeGuardRepository::save()now also checks the existing row's scope (new required entity-id accessor), blocking relabel-to-claim.TxRunnerfor atomic close-then-insert.to/fromAddressrejected if they contain CR/LF/NUL.Tests: expanded
test_repository_decorators(reparenting + queryable filtering) and added curl-guardedtest_smtp_transport; all 15 ctest targets pass.Note: the
ScopeGuardRepositoryconstructor gained a required entity-id accessor (breaking) — safe because consumers pin authkit by commit SHA and bump deliberately. The Medium/Low items remain open.The medium-severity findings are fixed in commit
fafee12onmain:cookieValuehelper) — no more substring shadowing ofsession=.IAuthBackend::hasActiveUsers()documented as must-fail-closed.ws/HubemptypropertyIdsnow = no access for non-admins (was "all").util/OriginCheck.hpp+ Hub doc — WSController must validate Origin at handshake (CSWSH).RedactedFieldRepositorythrows on an unknown redaction field name.RateLimiterctor validates capacity/refillRate (throws on 0/negative/NaN/inf).clientIpTrusted"unknown"/"invalid" shared-bucket caveat.util/SessionCookie.hpp— safe-by-default Set-Cookie builder.AuthInterceptorOrigin/Referer-vs-Host check on session mutations; cert path documented as non-browser / not CSRF-gated.AuthInterceptoroptional injected RateLimiter throttles invalid-token attempts → 429.M-5 (temporal non-atomic save) was already resolved by the H-4 fix.
Tests: 4 new suites (token_extract, rate_limiter, origin_check, session_cookie) + extended redacted-field; all 19 ctest targets pass.
fewo-webapp integration note (for the next pin bump): three are behaviour changes, not just additive — (1) WS non-admin sockets with an empty scope set now receive nothing (M-3); (2) session-cookie mutations with a cross-host Origin/Referer are now 403'd by authkit itself (M-10) — fewo's own CsrfStrictInterceptor already does this, so same-origin traffic is unaffected; (3) setup-mode requires a loopback bind (M-2). The
AuthInterceptorconstructor only gained an optional trailing param (non-breaking). Remaining: the Low findings.The low-severity findings are fixed in commit
9976efeonmain, and fewo-webapp's pin is bumped to it (fewo99dee97).requireUserguardsstd::stoi— malformed bundle id → clean 401, not a 500;AuthPrincipal::iddocumented as numeric-only.CURLUSESSL_ALL) for non-loopback relays (no silent STARTTLS-strip to cleartext).AuditLog::escapeJsonescapes all control chars (RFC 8259);SKIP_FIELDSnow excludes credential fields from diffs.ws/Hubconsumes thethread_localauth handoff once up-front and clears it unconditionally (no stale leak to a reused thread).resolveBySessionHashdocumented as the authoritative expiry gate.util/ConstantTime.hpp(constantTimeEquals) + TokenHasher ≥256-bit contract.likeEscape+Field::likeContains/likePrefixemitLIKE ? ESCAPE '\'; compile-time identifier-source invariant documented.Tests: new
test_constant_time; LIKE-escaping cases intest_queryable. All 20 authkit ctest targets pass.All actionable findings (5 High, 11 Medium [M-5 = H-4], 8 Low) are now resolved across H/M/L. Only the informational items remain (e.g. broad
img-srcCSP, the unscoped legacynotifyBookingoverload). Suggest closing this issue once those are triaged.Operational note for fewo: L-2 now refuses a non-loopback SMTP relay that can't STARTTLS (previously it would fall back to cleartext). If the production relay is non-loopback, confirm it supports STARTTLS or front it via a localhost relay.