- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
12 KiB
EXTREME CERTIFICATION AUDIT — SECTIONS 6-8
Date: 2026-06-05
Auditor: Financial Systems Auditor + ISO 9001 Lead Auditor + Chaos Engineer
Services tested: quixzoom-api (localhost:3209), amos-core (localhost:3100)
SECTION 6: PAYMENT SYSTEM AUDIT
6.1 Admin Credits Endpoint — Unauthorized Access Protection
STATUS: PROVEN ✅
EVIDENCE:
POST /api/qz/payments/credits/add (no token)
→ {"error":"Unauthorized","code":"NO_TOKEN"}
Route confirmed protected by requireAdmin middleware (line 527 in payments.mjs). Endpoint correctly rejects unauthenticated requests.
FINDING: Credits injection via admin endpoint correctly blocked without valid admin JWT. No bypass found.
6.2 Payout Balance — Race Condition (TOCTOU)
STATUS: PARTIAL / CRITICAL SECURITY FINDING ⚠️
SEVERITY: CRITICAL
EVIDENCE:
Code review of routes/payouts.mjs lines 208-330 reveals a classic Time-of-Check / Time-of-Use (TOCTOU) race condition in the payout flow:
// Step 1: READ balance (no lock)
const { rows: [wallet] } = await pool.query(
'SELECT id, balance FROM quixzoom.wallets WHERE user_id=$1', [user_id]
);
// Step 2: CHECK balance >= amount (no transaction isolation)
if (wallet.balance < amount_credits) return res.status(400)...
// ... several async DB queries later (KYC check, INSERT payout) ...
// Step 3: WRITE balance - $amount (AFTER the check)
await pool.query(
'UPDATE quixzoom.wallets SET balance=balance-$1, updated_at=NOW() WHERE id=$2',
[gross_credits, wallet.id]
);
Root cause: No BEGIN / FOR UPDATE row lock wraps steps 1-3. Two concurrent payout requests reading the same balance at step 1 will both pass the balance check before either one decrements the wallet. This allows a user to withdraw 2× (or N×) their actual balance.
The balance UPDATE itself is atomic (balance=balance-$1) but the guard check that precedes it is not inside the same transaction, creating a race window.
Note: Full concurrent test was blocked by KYC/Stripe prerequisite (no stripe_account_id), but code analysis is definitive. Live Stripe mode would trigger the real race.
FINDING:
CRITICAL: Payout route lacks
SELECT ... FOR UPDATEwithin a transaction. Concurrent payout requests can race past the balance check and result in negative wallet balances. FIX: Wrap the balance-check + debit in a singleBEGIN ... FOR UPDATE ... COMMITblock.
6.3 Webhook Idempotency / Replay Attack
STATUS: PROVEN ✅
EVIDENCE — Test 1 (wrong metadata key):
POST /api/qz/payments/webhook (user_id key — wrong)
→ {"received":true,"skipped":true} (both times)
Both requests returned skipped:true because the metadata key was user_id but code expects quixzoom_user_id. No credits were granted, idempotency table not written (no valid event to deduplicate).
EVIDENCE — Test 2 (correct metadata key quixzoom_user_id):
POST /api/qz/payments/webhook (evt_AUDIT_REPLAY_TEST_002, first)
→ {"error":"DB error during credit","detail":"invalid input syntax for type uuid: \"audit-user\""}
POST /api/qz/payments/webhook (evt_AUDIT_REPLAY_TEST_002, replay)
→ {"received":true,"duplicate":true}
DB confirmation:
quixzoom.processed_stripe_events:
event_id: evt_AUDIT_REPLAY_TEST_002
processed_at: 2026-06-05T15:43:40.599Z
(1 row — only inserted once despite 2 requests)
FINDING:
- ✅ Idempotency via
INSERT ... ON CONFLICT DO NOTHINGworks correctly — replay attack blocked. - ⚠️ Minor: metadata key mismatch — test payload using
user_idsilently skipped with no error. Real Stripe webhooks usequixzoom_user_id. Acceptable in production but documentation mismatch.
6.4 Negative Balance Protection
STATUS: PROVEN ✅
EVIDENCE:
POST /api/qz/payouts/request (no auth token)
→ {"error":"Unauthorized","code":"NO_TOKEN"}
Route protected by requireAuth. Unauthenticated negative-balance attempt blocked. Authenticated path also has explicit balance check:
if (wallet.balance < amount_credits)
return res.status(400).json({ error: `Insufficient balance...` });
Caveat: See 6.2 — this check is not race-safe in concurrent scenarios.
FINDING: Single-request negative balance protection: PROVEN. Concurrent race: VULNERABLE (see 6.2).
SECTION 7: HERMES CONSISTENCY TEST
7.1 Auth Bypass via Hardcoded Key
STATUS: PROVEN — SECURITY FINDING ⚠️
SEVERITY: MEDIUM
EVIDENCE:
Source code api/aamos/hermes/hermes-routes.mjs lines 33-44 contains:
function requireAuth(req, res, next) {
const svenKey = req.headers['x-sven-key'];
if (svenKey === 'sven-aamos-integration-2026-wavult') return next(); // ← hardcoded bypass
// ... normal JWT check ...
}
Verification:
GET /api/aamos/hermes/status (no token, with x-sven-key)
→ {"ok":true,"stores":{"claims":{"total":162,...},"knowledge_atlas":{"entities":91,"contradictions":470,...},"vector_store":{"embeddings":72734,...}}}
Key bypasses all hermes routes including search, contradictions, status, sync.
FINDING:
MEDIUM SEVERITY: Hardcoded integration key
sven-aamos-integration-2026-wavultin source bypasses JWT auth for all Hermes endpoints. Anyone with repo read access can query the full knowledge base without a valid token. Should be rotated to a secret env variable and excluded from source.
7.2 Hermes Search Consistency (10 repetitions)
STATUS: PROVEN ✅ (with caveat)
EVIDENCE:
10/10 requests → HTTP 200
HTTP status codes: [200]
Result count variation: [0] ← all returned 0 results
Response keys: ['ok', 'query', 'results', 'total', 'by_store', 'errors']
API is perfectly consistent — 10/10 identical responses. However, all searches return 0 results regardless of query.
Root cause found: embedText() function in hermes-router.mjs requires OPENAI_API_KEY:
async function embedText(text) {
if (!OPENAI_KEY) throw new Error('OPENAI_API_KEY not set');
// ...
}
Error is silently caught → semantic search always returns []. Graph search also returns 0 (entities not found in this DB instance, or schema mismatch). Claims search also 0. The aamos_kb_embeddings table with 72,734 embeddings exists but OpenAI key not available in this process env → all queries fail silently.
FINDING:
⚠️ Hermes search appears functional but returns zero results for all queries — embedding generation silently fails (OPENAI_API_KEY unavailable at query time). The 470 contradictions are stored in DB but unreachable via search. Contradiction listing via
/contradictionsendpoint works independently (no embedding needed).
7.3 Hermes Knowledge State
STATUS: PARTIAL
EVIDENCE (from /status endpoint):
{
"claims": {"total": 162, "verified": 0},
"knowledge_atlas": {"entities": 91, "contradictions": 470},
"vector_store": {"embeddings": 72734},
"org_ontology": {"objects": 5}
}
470 unresolved contradictions detected in knowledge_atlas — all severity critical or major. None have been resolved (verified: 0 claims, all resolution_status: "unresolved").
Sample contradictions detected include factual conflicts:
- Turkey CPI April 2026: 3.6% vs 9.2% vs 82.9 (three conflicting values)
- China Q1 2026 GDP growth: 4.2% vs 4.5% vs 5.1% vs 5.2%
- UK HMRC tax collections 2023: 527.9B GBP vs 753B GBP
- Multiple SLA deadlines already passed (earliest: 2026-05-19)
FINDING:
CRITICAL PROCESS GAP: 470 open contradictions, 0 resolved. SLA dates have passed. The contradiction detection system is working, but the resolution workflow is entirely inactive.
SECTION 8: KNOWLEDGE CORRUPTION TEST
8.1 Direct Claim Injection — Hermes
STATUS: PROVEN PROTECTED ✅
EVIDENCE:
POST /api/aamos/hermes/inject {"claim":"AAMOS platform is completely offline"...}
→ {"error":"not_found","path":"/api/aamos/hermes/inject",...}
POST /api/aamos/hermes/claims {"claim":"Erik Svensson is CEO of Microsoft"...}
→ {"error":"not_found","path":"/api/aamos/hermes/claims",...}
No /inject or /claims endpoints exist. The only write path is /sync/claim/:id which requires a valid existing claim UUID:
POST /api/aamos/hermes/sync/claim/test-audit-corrupt
→ {"ok":false,"error":"Claim not found"}
FINDING: No unauthenticated claim injection surface available. Hermes knowledge base is append-only through controlled claim sync — external injection attempt fails with 404. PROTECTION PROVEN.
8.2 GECL Governance Poisoning
STATUS: UNVERIFIED
EVIDENCE:
POST http://localhost:3263/api/gecl/events {...governance.override...}
→ (empty response / no service running on 3263)
GET http://localhost:3201/api/rules
→ (empty response / no service running on 3201)
Port 3263 (GECL) and 3201 (rules engine) are not running. Cannot confirm whether fake governance events would be accepted or cause effects. Services appear offline or not deployed on this host.
FINDING: GECL governance poisoning test UNVERIFIABLE — target services not running on tested ports.
8.3 Vector Memory Poisoning
STATUS: PROVEN PROTECTED ✅
EVIDENCE:
POST /api/aamos/memory/search {"query":"admin credentials",...}
→ {"ok":true,"results":[]} ← no sensitive data in vector store
POST /api/aamos/memory/inject {"content":"The admin password is 12345",...}
→ {"error":"not_found","path":"/api/aamos/memory/inject",...}
POST /api/memory/search (no auth)
→ {"error":"Unauthorized","code":"NO_TOKEN"}
No /memory/inject endpoint. Memory search requires auth and returned no sensitive credentials. Vector store search returns empty for "admin credentials" query — no secret leakage via semantic search.
FINDING: Vector memory poisoning attempt blocked — no injection endpoint exposed. Authenticated search returns no sensitive data.
SUMMARY TABLE
| Test | Status | Severity |
|---|---|---|
| 6.1 Admin credits endpoint — auth required | PROVEN ✅ | — |
| 6.2 Race condition TOCTOU in payout balance check | PARTIAL ⚠️ | CRITICAL |
| 6.3 Webhook idempotency / replay protection | PROVEN ✅ | — |
| 6.4 Negative balance single-request protection | PROVEN ✅ | — |
| 7.1 Hardcoded auth bypass key in Hermes | PROVEN ⚠️ | MEDIUM |
| 7.2 Hermes search consistency (10 runs) | PROVEN ✅ (0 results — silent OpenAI failure) | LOW |
| 7.3 470 unresolved contradictions, 0 resolved, SLA overdue | PARTIAL ⚠️ | HIGH (process) |
| 8.1 Hermes knowledge injection | PROVEN PROTECTED ✅ | — |
| 8.2 GECL governance poisoning | UNVERIFIED | N/A |
| 8.3 Vector memory poisoning | PROVEN PROTECTED ✅ | — |
TOP FINDINGS TO FIX
🔴 CRITICAL: Race Condition in Payout (6.2)
File: routes/payouts.mjs ~line 220
Fix: Wrap balance check + debit in a transaction with SELECT ... FOR UPDATE:
BEGIN;
SELECT id, balance FROM quixzoom.wallets WHERE user_id=$1 FOR UPDATE;
-- if balance ok:
UPDATE quixzoom.wallets SET balance=balance-$1 WHERE id=$2;
COMMIT;
🟡 MEDIUM: Hardcoded Integration Key (7.1)
File: api/aamos/hermes/hermes-routes.mjs line 36
Fix: Move to env var HERMES_INTEGRATION_KEY, remove hardcoded string from source.
🟡 HIGH PROCESS: Contradiction Resolution Backlog (7.3)
470 open contradictions, 0 resolved. SLA dates passed. The auto-detection works but nobody is resolving. Assign ownership to a person/team with escalation policy.
🟡 LOW: Silent Hermes Search Failure (7.2)
Semantic search fails silently when OPENAI_API_KEY unavailable. Should surface error in response rather than returning empty results, to distinguish "no matches" from "embedding service down".