MemorySmith External Audit — Final Council Review
Council Composition: 5 Specialist Chairs + 2 Anonymous Peer Reviewers + Chairman
Process: 2-round independent review with per-chair peer validation
Date: 2026-06-17
Source audit: Data/Pages/audits/external-deep-research-audit-20260617.md
Verdict Authority: Two complete review rounds; all Round 1 open questions closed by Round 2 forensic verification
Executive Summary
The external audit identified directionally important problems (missing antiforgery, OAuth bootstrap weakness, missing HSTS, hardcoded paths) but surrounded those real findings with factual errors, mislabelled severities, references to code that does not exist, and at least one outright fabricated CVE chain. Round 2 forensic verification confirmed two CRITICAL issues (leaked production secrets in version control; unauthenticated first-user-becomes-Admin OAuth bootstrap), four HIGH issues (universal antiforgery gap including a CSRF-to-shell vector; anonymous Editor escalation via OpenLocalEditor + null-loopback; live MemoryIndex concurrency race; OAuth ClientSecret rotation hygiene), and several MEDIUM/LOW items.
The audit itself is graded C-minus: useful as a checklist trigger, unsafe as a primary source. Roughly one-third of its specific technical claims are false, but the genuine findings it surfaced are severe enough that the document warranted council review.
Part I: Verdicts on Original Audit Claims
| # | Audit Claim | Final Verdict | Evidence Summary |
|---|---|---|---|
| 1 | SQLite CVE-2025-6965 / CVE-2025-70873 exposure | REFUTED | bundle_e_sqlite3 v3.0.3 transitively bundles SourceGear.sqlite3 ≥ 3.50.4.5 (SQLite 3.50.4). CVE-2025-6965 fixed in 3.50.2 — bundled version is newer. CVE-2025-70873 affects only the zipfile CLI extension not compiled into Microsoft.Data.Sqlite. Not vulnerable. |
| 2 | Antiforgery "enabled for form actions" | REFUTED | Audit mischaracterized the codebase. Zero [ValidateAntiForgeryToken] or [AutoValidateAntiforgeryToken] across all 11 controllers. No partial protection — there is no protection. The actual vulnerability is more severe than the audit claimed. |
| 3 | Missing HSTS headers | CONFIRMED | No app.UseHsts() call; no Strict-Transport-Security header emitted anywhere in the middleware pipeline. |
| 4 | Server: Kestrel header exposure |
CONFIRMED | Default Kestrel emits the server banner; no AddServerHeader = false configured. |
| 5 | CSP headers present | CONFIRMED (positive) | CSP middleware is present and applied. Configurable defaults. Audit correctly noted this as a strength. |
| 6 | ApiKey: null + AllowRemoteApi: false defaults |
CONFIRMED (positive) | appsettings.json ships with both. Remote API blocked at transport layer by default. |
| 7 | Missing exception handling in background tasks | REFUTED | MemoryMaintenanceService wraps work in RunTrackedAsync with structured logging and full try/catch. Every inspected BackgroundService has the same pattern. |
| 8 | Serilog has no retainedFileCountLimit |
PARTIAL | Main logger configures retainedFileCountLimit: 14. Bootstrap (early-startup) logger does not — small disk-fill window during startup logging only. |
| 9 | Hardcoded paths in tests | CONFIRMED | Multiple test and configuration files contain CWD-relative Path.Combine("..", "Data", ...) fallbacks. |
| 10 | NuGet packages out of date (specific versions cited by audit) | REFUTED | Audit cited Serilog.AspNetCore 6.0.1 (actual: 10.0.0) and Swashbuckle 6.10.0 (actual: 10.1.7). Wrong line-by-line. |
| 11 | "Bridge is ~3K LOC WebSocket service" | REFUTED | Bridge is 720 lines, CLI-to-MCP-via-HTTP-POST. No WebSocket code exists anywhere in the project. |
| 12 | "ONNX models committed under MemorySmith.Core/Models" | REFUTED | Location is Data/Models/ (gitignored); the correct filename is e5-base-v2.onnx, not embedding-model.onnx. |
| 13 | "Lucene full-text search" | REFUTED | No Lucene index, BM25, or full-text IndexSearcher. Only Lucene's StandardAnalyzer tokenizer is used for token extraction. Scoring is a hand-rolled token-overlap implementation. |
| 14 | Hybrid search exists | CONFIRMED | Vector + lexical hybrid scorer is real. Memory path uses RRF; code search path uses weighted linear combination. |
| 15 | "Test coverage unknown" | REFUTED (procedurally) | CI emits Cobertura + HTML coverage as artifacts on every push. No threshold gate exists, but coverage is measured. |
Tally: 5 CONFIRMED, 7 REFUTED, 1 PARTIAL, 2 CONFIRMED-as-positive.
Part II: Final Finding Register
All findings — from the original audit (where confirmed) plus council discoveries — ordered by severity.
CRITICAL
C-1. Production credentials committed to public repository
First found by: Chairs A and B independently.
Locations:
- artifacts/MemorySmith.App/appsettings.LocalOverrides.json — committed 80002eb4 on 2026-05-30; public 18+ days.
- .vscode/mcp.json — committed 2840bd23 on 2026-05-27; second independent exposure.
Exposed secrets:
- MemorySmith:ApiKey = DLxQJGounPrxit_ITQlcHBBbXLAbaryl-oduvmQMphw — the shared secret gating /api/* and /mcp when AllowRemoteApi=true. Grants Viewer/Editor access, NOT Admin.
- GitHub OAuth ClientId = Ov23liSOQ3m2pthdQv0w — real post-2023 Ov23li prefix format.
- GitHub OAuth ClientSecret = 9d0b9498af3f872b7cb1289b175498da2ffccdb8 — 40-hex format consistent with real GitHub OAuth secrets.
Aggravating factors: .gitignore lists both source paths but files were force-added despite ignore rules. No rotation visible in git history. The audit missed this entirely.
C-2. First OAuth user automatically becomes Admin (unauthenticated bootstrap)
First found by: Chair A.
Location: GitHubOAuthCallbackHandler.cs:115 — first GitHub-authenticated user is elevated to Admin with zero gating (no IP allowlist, no bootstrap token, no email allowlist).
Reachability: OAuth challenge endpoints (/api/auth/challenge?scheme=GitHub) are NOT loopback-gated. Any HTTPS caller can initiate the OAuth flow.
Composability with C-1: The leaked OAuth ClientSecret in C-1 amplifies this. An attacker who can intercept the OAuth callback against a server that has never been bootstrapped can plausibly claim Admin.
HIGH
H-1. Universal CSRF gap including CSRF-to-shell vector
First found by: Chair A (Round 1 observation), confirmed exhaustively by Chair A Round 2.
Scope: Zero antiforgery enforcement across all 11 controllers and 27+ POST/PUT/DELETE endpoints including:
- SourceLinksController.Open — can be CSRF'd to invoke the OS default-app handler (CSRF-to-shell)
- AdminController.AssignRole / RemoveRole — CSRF privilege assignment
- MemoriesController, PagesController, TasksController — full data mutation
- MaintenanceAgentController.RunNow / RunWeekly — trigger background jobs
Mitigating factor for remote: AllowRemoteApi=false default blocks remote callers at transport layer. Still HIGH because malicious local pages and localhost callbacks can cross-origin-POST.
H-2. Anonymous Editor escalation via OpenLocalEditor + null-loopback
First found by: Chair A.
Composition: OpenLocalEditorCompatibility=true (default in appsettings.json and MemorySmithOptions.cs) + IsLoopback(null) returns true in MemorySmithRequestGuardMiddleware.cs. When an upstream proxy strips RemoteIpAddress to null (mTLS, unix-socket, misconfigured reverse proxy), anonymous callers receive Editor-level authority — sufficient to write memories, write pages, trigger maintenance, use chat with agent writes.
Important bound: RequiresAuthenticatedAdmin early-out is real and prevents this from escalating to Admin. This is an Editor-escalation only.
H-3. Live concurrency race in MemoryIndex
First found by: Chair C. Confirmed by Chair A Round 2.
Location: MemorySmith.Core/Indexing/MemoryIndex.cs — three plain Dictionary<,> fields, no locks, no ConcurrentDictionary. Registered as singleton (AddSingleton<MemoryIndex>()).
Concurrent write paths verified:
- MemoryApplicationService.cs:549 — Add from HTTP POST /memories
- MemoryApplicationService.cs:588-589 — Remove + Add from HTTP PUT /memories/{id}
- MemoryApplicationService.cs:624 — Remove from HTTP DELETE
- MemoryApplicationService.cs:677-678 — Remove + Add from touch/UsageCount (very frequent)
- MemoryMaintenanceTasks.RunIndexRebuildAsync() — Clear() then full re-Add from snapshot, called periodically by MemoryMaintenanceService : BackgroundService
Worst pattern: Rebuild() does Clear() then refills from a pre-captured snapshot. Any web-request Add() between Clear() and the refill is silently lost.
Current impact bounded by: Search currently uses _store.LoadAll(), not the index. Impact escalates to CRITICAL the moment search is promoted to consume the index.
H-4. OAuth ClientSecret rotation hygiene
First found by: Council (composes with C-1). Issue: A leaked OAuth ClientSecret participates in GitHub's token exchange flow. Rotation requires both a new secret in GitHub and deployment update. Not yet rotated per git history.
MEDIUM
M-1. Missing HSTS
No app.UseHsts() and no Strict-Transport-Security header emitted. Leaves SSL-strip and downgrade attacks in scope for any HTTPS deployment. Trivial one-line fix.
M-2. Server: Kestrel banner exposed
Information disclosure of server technology. Set AddServerHeader = false in Kestrel options.
M-3. FixedTimeEquals length-leak before constant-time compare
Locations: MemorySmithRequestGuardMiddleware.cs:96-104 AND SecurityServices.cs:344-356 (both copies).
Pattern: actualBytes.Length == expectedBytes.Length && CryptographicOperations.FixedTimeEquals(...) — C# && short-circuits, leaking whether lengths match via response timing.
Practical impact: LOW. The configured ApiKey is 32+ bytes of entropy; length-disclosure does not reduce keyspace meaningfully.
Why M not L: The code reads as "we used FixedTimeEquals therefore we're constant-time" when it isn't. Every subsequent reviewer will reflag it. Fix is trivial: pad both sides to a common length before comparing.
M-4. Hardcoded CWD-relative path fallbacks
Path.Combine("..", "Data", ...) used as fallback throughout hosting setup (MemorySmithStorageSetup.cs, MemorySmithSecuritySetup.cs, MemorySmithContentEndpoints.cs, TrainingOptions.cs, MaintenanceAgentServices.cs). Configurable overrides exist but fallbacks depend on working directory at process launch — known to have caused Windows Service runtime issues in prior deployments (per in-repo audit history).
M-5. Bootstrap Serilog has no retainedFileCountLimit
Program.cs:23-26 early-startup logger writes without retention policy. Small but real disk-fill window during repeated crash-before-host-build cycles.
M-6. No coverage threshold gate
CI publishes Cobertura + HTML per push but sets no minimum. Coverage can regress without breaking a build. No coverage badge in README.
M-7. No packages.lock.json
Dependency graph is not locked. SourceGear.sqlite3 floor of >= 3.50.4.5 currently resolves above all known CVE fix points, but a future NuGet snapshot could pull a regressed transitive version.
M-8. ONNX model provenance has no integrity verification
Models pulled from HuggingFace via PowerShell+Python install script. No hash check, no lockfile, no signature verification. Models are gitignored — no audit trail of which version is in use. Acceptable for single-user local tool; unacceptable for any shared/production deployment.
LOW / IMPROVEMENT
L-1. TSK-0280 (shared WebApplicationFactory) unresolved
7+ distinct factory setups duplicated across integration tests. Maintenance and test-isolation cost. Pre-existing tracked task.
L-2. Bridge CLI reaches /mcp without credentials under default config
ApiKey=null default + AnonymousAccess=Viewer means any local process can call read-only MCP tools without authenticating. Intentional design for local-first; flag for remote-enabled deployments.
L-3. PowerShell CI data-validation scripts were not mentioned in the audit
Test-TaskRecords.ps1, Test-MemoryRecords.ps1, Test-PageLinks.ps1, Test-PagePathLiterals.ps1 all run as hard-stopping gates before the build step (all use $ErrorActionPreference = 'Stop'). These represent a test layer the audit missed when claiming coverage was "unknown."
Part III: Audit Quality Assessment
Overall Grade: C-minus
Rationale. The audit successfully triggered review of two genuinely critical issues (C-1: leaked credentials, C-2: OAuth bootstrap) and a cluster of valid medium items (HSTS, server header, hardcoded paths, CSP verification). Those finds alone justify the document's existence.
However, the audit also: - Fabricated a CVE chain (SQLite vulnerabilities that do not apply to the bundled version, with the recommendation to switch to the less safe package). - Invented technical details (WebSocket bridge, Lucene full-text search, ONNX model location) that do not exist in the codebase. - Cited wrong NuGet versions (4+ major versions stale for a .NET 10 project). - Misread the antiforgery state — claimed "partial protection exists" when there is in fact no protection (an error that understated the real severity). - Dismissed coverage as "unknown" without looking at the CI workflow that publishes coverage on every push.
Pattern diagnosis: A checklist-driven review with limited code-reading discipline. The auditor correctly identified the classes of risk to look for but verified against templates rather than against actual source files. The Bridge LOC fabrication (720 lines reported as ~3K), the wrong ONNX path, and the wrong NuGet versions together suggest portions of the report were generated from project descriptions or templates rather than from direct file inspection.
Reuse policy: Treat findings from this audit as directional hints requiring independent verification. Do not carry specific version numbers, file paths, or CVE references forward without re-checking them. The directional HSTS / server-header / secrets-hygiene signals are accurate and worth keeping.
Part IV: Recommended Actions
P0 — Must do before next deployment (within 24 hours)
- Rotate all credentials from C-1. Generate a new
MemorySmith:ApiKey. Generate a new GitHub OAuth ClientSecret. Update all deployments. Treat both as compromised. - Purge secrets from git history. Use
git filter-repoor BFG againstartifacts/MemorySmith.App/appsettings.LocalOverrides.jsonand.vscode/mcp.json. Force-push. Add those paths to.gitignore(already listed; fix the force-add workflow). - Gate OAuth first-user-is-Admin bootstrap (C-2). Replace the implicit promotion at
GitHubOAuthCallbackHandler.cs:115with: (a) bootstrap token required, or (b) email/username allowlist, or (c) loopback-only first-login. Any gating mechanism closes this. - Add global antiforgery enforcement (H-1). Apply
[AutoValidateAntiforgeryToken]globally inProgram.csor per-controller. Highest urgency onSourceLinksController.Open(OS shell vector) andAdminController(role assignment).
P1 — This sprint
- Fix
MemoryIndexthread safety (H-3). Swap internalDictionary<,>forConcurrentDictionary<,>and rewriteRebuild()to swap atomically (build a new dictionary, then swap the reference rather thanClear()-then-refill). - Close the null-loopback escalation (H-2). Either fail-closed on
RemoteIpAddress=null(treat null as non-loopback), or require explicit opt-in configuration. - Add HSTS and disable the Server banner (M-1, M-2). One-line fixes each.
- Fix
FixedTimeEqualsin both copies (M-3). Pad to common length before callingCryptographicOperations.FixedTimeEquals.
P2 — This month
- Adopt
packages.lock.json(M-7). Lock the dependency graph; re-verify SQLite version under the lock. - Add coverage threshold gate to CI (M-6). Pick a current-baseline floor; add badge to README.
- Add hash verification to ONNX install script (M-8). SHA-256 check on downloaded model files; fail install on mismatch.
- Resolve TSK-0280 (L-1). Consolidate integration test factory.
- Write a SECURITY.md. Document the local-first threat model, the AnonymousAccess defaults, OAuth bootstrap procedure post-fix, and the rotation procedure for API key and OAuth credentials.
P3 — Backlog
- Bootstrap Serilog retention (M-5). Add
retainedFileCountLimitto the early-startup logger. - Replace hardcoded CWD path fallbacks (M-4). Use
AppContext.BaseDirectoryor an explicitMemorySmith:DataPathconfiguration requirement at startup. - Add regression tests for each closed P0/P1 finding. One test per resolved vulnerability demonstrating the fix.
Part V: Round 2 Open Questions — Closure Record
| Q | Round 1 Question | Round 2 Answer |
|---|---|---|
| Q1 | Are leaked credentials real? Git history? What does ApiKey gate? | CONFIRMED REAL. Two files, committed 2026-05-27 and 2026-05-30. ApiKey gates /api/* and /mcp when AllowRemoteApi=true (not Admin paths). No rotation in history. |
| Q2 | Is bundled SQLite actually patched? | NOT VULNERABLE. SourceGear.sqlite3 ≥ 3.50.4.5 ships SQLite 3.50.4 ≥ CVE fix point 3.50.2. CVE chain is inapplicable. |
| Q3 | Antiforgery gap universal across all controllers? | YES, EXHAUSTIVELY CONFIRMED. All 11 controllers, 27+ endpoints, zero attributes. Full controller inventory in H-1. |
| Q4 | Is the OAuth attack chain remotely exploitable? | TWO CHAINS. Chain 1 (Editor via null-loopback): real, bounded to Editor, cannot reach Admin. Chain 2 (Admin via OAuth first-login): internet-reachable on un-bootstrapped servers. |
| Q5 | Is FixedTimeEquals length-leak real? |
CONFIRMED in two files. Low practical impact; trivial fix. |
| Q6 | Are there live concurrent MemoryIndex write paths? | CONFIRMED LIVE RACE. Five web-request write sites + periodic BackgroundService Clear()-then-refill. Not "write-once at startup." |
| + | Actual coverage percentage? | CI emits Cobertura artifacts per push. No threshold gate; no badge. Exact numbers require CI artifact download — not committed to repo. |
| + | PowerShell scripts are real CI gates? | YES. All four use $ErrorActionPreference = 'Stop' + throw/exit 1. Run before build; a failure aborts the job. |
| + | Bridge authentication requirement? | ApiKey opt-in. Default ApiKey=null means local callers reach read-only MCP endpoints without credentials. Remote callers blocked by AllowRemoteApi=false. |
| + | TSK-0280 status? | UNRESOLVED. 7+ distinct factory setups; no shared factory class. |
| + | AnonymousAccess default? |
"Viewer" confirmed in appsettings.json. Intentional for local-first design; bounded by transport-layer remote block. |
| + | ONNX model provenance? | No integrity verification. HuggingFace pull via PowerShell+Python. No hashes, no lockfile, no signatures. Models gitignored. |
Part VI: Council Dissent Record
Dissent A — FixedTimeEquals severity (M-3):
Chair A argued LOW (32-byte key, length disclosure does not reduce keyspace on real hardware); Peer Reviewer B argued MEDIUM (the code reads as constant-time when it isn't — future reviewers will repeatedly flag it). Resolution: MEDIUM with LOW practical-impact note preserved.
Dissent B — Audit grade (C vs. D): Peer Reviewer A argued D (fabricated CVE chain, invented Bridge architecture, wrong NuGet versions). Peer Reviewer B argued C (did correctly surface three genuinely critical issues). Resolution: C-minus, with explicit instruction that the audit is directional-hint-quality, not primary-source-quality.
Dissent C — MemoryIndex race severity (HIGH vs. CRITICAL):
Chair A (Concurrency view) argued CRITICAL (data-loss pattern is live). Chair C (Storage view) argued HIGH (search doesn't yet consume the index; impact bounded today). Resolution: HIGH with explicit note that severity escalates to CRITICAL the moment search is promoted to consult the index. P1 priority unchanged.
Dissent D — OAuth Chain 2 severity (HIGH vs. CRITICAL): Chair A argued CRITICAL (zero-gating first-Admin path + leaked ClientSecret = internet-reachable Admin takeover on un-bootstrapped server). Peer Reviewer A argued HIGH (conditional — requires un-bootstrapped target). Resolution: CRITICAL (C-2) — composability with C-1 (leaked secret) was the deciding factor. The combination of no bootstrap gating + leaked OAuth credentials exceeds the "conditional" threshold for CRITICAL.
Meta-dissent (Peer Review): Both anonymous peer reviewers independently observed that the council itself in Round 1 accepted the audit's "no antiforgery anywhere" claim before Round 2 verified the exhaustive controller sweep. The lesson — apply the same skepticism to council output that the council applied to the audit — is recorded for future reviews.
Report produced by 5-chair council with 2 anonymous peer reviewers and Chairman synthesis across 2 rounds. Commit this document to Data/Pages/council/external-audit-council-review-20260617.md.