Entry for: MUIAA Ltd × Salamander Community — ChamaConnect Virtual Hackathon
Theme: Reimagining Digital Chamas for the Future
Deadline: Friday, 2026-04-24 at 23:59 EAT
Entrant: Eugene Mutembei (eugenegabriel.ke@gmail.com) & Sidney Muriuki (sidneybarakamuriuki1@gmail.com)
What we built: an M-Pesa-native contribution auto-reconciliation module that closes the single biggest gap chamaconnect.io has today — the feature its own features page marks "Coming Soon." Drop-in Next.js, double-entry ledger, idempotent Daraja callbacks, USSD access for feature phones, on-chain anchoring of daily settlements.
Chama-Connect/
├── README.md ← you are here
├── CONTRIBUTING.md ← how to contribute, PR checks, bug workflow
├── LICENSE
├── .env ← login creds to chamaconnect.io (gitignored)
├── .env.example ← root env template (recon)
│
├── recon/ ← Playwright recon of the live platform
│ ├── tests/explore.spec.ts ← logs in, crawls dashboard, records every XHR
│ └── artifacts/<timestamp>/ ← screenshots, HTML, network logs per run
│
├── bugs/ ← bug register (evidence + root cause + fix)
│ ├── README.md ← index + severity scale
│ ├── _template.md ← filing template
│ └── BUG-NNN-*.md ← one file per bug (001–076 today)
│
├── chamapay/ ← the deliverable (standalone Next.js app)
│ ├── src/
│ │ ├── app/ ← Next.js App Router: UI + API routes
│ │ │ ├── chamas/[code]/ ← live dashboard
│ │ │ └── api/mpesa/... ← Daraja webhooks
│ │ ├── lib/
│ │ │ ├── daraja/ ← STK Push, C2B register, B2C, TxStatus
│ │ │ ├── reconciliation/ ← deterministic matching engine + tests
│ │ │ ├── anchor/ ← Merkle tree + Base Sepolia anchor CLI
│ │ │ ├── sms/ ← async outbox
│ │ │ └── db/ ← SQLite schema, migrate, seed
│ │ └── ...
│ ├── var/chamapay.sqlite ← local DB (created by migrate)
│ └── .env.example
│
└── docs/
├── TECHNICAL-PROPOSAL.md ← judges' technical write-up
├── DEMO.md ← 90-second demo script
└── Anchor.sol ← reference on-chain contract
git clone <this-repo>
cd Chama-Connect/chamapay
cp .env.example .env.local # Daraja creds optional for local demo
npm install
npm run db:migrate && npm run db:seed
npm run dev # http://localhost:3100Open http://localhost:3100/chamas/ACME.
Step-by-step narration for judges: docs/DEMO.md.
In another shell, fire a simulated M-Pesa payment through the real reconciliation engine:
curl -X POST http://localhost:3100/api/dev/simulate-c2b \
-H 'content-type: application/json' \
-d '{"msisdn":"254711223344","amount":500,"billRef":"ACME-202604"}'The dashboard updates within a few seconds; the payment shows as matched at 100% confidence to member Brian Otieno for cycle 2026-04.
Run the test suite:
npm test
# 6 reconciliation tests: exact match, idempotency, MSISDN fallback,
# unmatched path, double-entry balance, mixed-format period parsing.From chamaconnect.io/features:
M-pesa Blockchain Integration — M-pesa and bank integration (Coming Soon) will enable seamless deposits, withdrawals, and loan repayments.
In Kenya, ~99% of chama money moves on M-Pesa. Without reconciliation, every chama admin still reads their M-Pesa SMS inbox line-by-line and types amounts into the platform manually — which is exactly the mechanism behind FSD Kenya's documented 13% chama embezzlement rate.
We built the fix in this repo. See docs/TECHNICAL-PROPOSAL.md for the full write-up.
Each row links to a standalone report (evidence, impact, root cause, proposed fix, verification). The canonical index and filing workflow live in bugs/README.md.
| ID | Title | Severity | Status |
|---|---|---|---|
| BUG-001 | Every public page ships with default Next.js boilerplate <title> + <meta description> |
High | Open |
| BUG-002 | Footer Features, Pricing, Resources, Blog, Community, Events all point to # |
Medium | Open |
| BUG-003 | Contact page phone number does not match footer / hackathon-brief contact number | Medium | Open |
| BUG-004 | Contact page renders literal [email protected] instead of an email address |
Medium | Fixed (2026-04-20) |
| BUG-005 | Login has no 2FA, no phone OTP, no social login — for a money platform | High | Open |
| BUG-006 | Register country selector defaults to International despite Kenya focus |
Low | Open |
| BUG-007 | M-Pesa integration marked "Coming Soon" — the #1 Kenyan chama requirement | Critical | Open → fixed by ChamaPay module |
| BUG-008 | POST /users/signin returns "message": "User Created" on every login |
High | Open |
| BUG-009 | MERRRY_GO_AROUND typo (triple-R, wrong phrase) in group-types endpoints and Create Chama dropdown |
High | Open |
| BUG-010 | Two different group-types endpoints with inconsistent (swapped) schemas |
High | Open |
| BUG-011 | Notifications page opens ws://localhost:3080 in production — real-time notifications broken |
Critical | Open |
| BUG-012 | /admin/chamas throws TypeError: Failed to fetch and shows "create your first chama" on network errors |
High | Open |
| BUG-013 | Signin returns the raw JWT in the response body (also in httpOnly cookie) — XSS-to-takeover path | High | Open |
| BUG-014 | /contact throws React error #418 (hydration mismatch) |
Medium | Open (not reproduced 2026-04-20) |
| BUG-015 | Every role record has permissions: [] — authz likely enforced by role name only |
High | Open |
| BUG-016 | Signin JWT has no exp/nbf/jti/iss/aud — tokens never expire, can't be revoked |
Critical | Open |
| BUG-017 | No Content-Security-Policy; HTML documents ship without HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy / Permissions-Policy |
High | Open |
| BUG-018 | Signin rate limit is 1000 req / 15 min per IP, no per-account lockout — brute-force viable | High | Open |
| BUG-019 | /api/auth/token returns 401 on every public page load (should be 200 with {token:null}) |
Medium | Open |
| BUG-020 | /api/proxy/users/current-user message says "Successfully retrieved logged in user" (double space) |
Low | Open |
| BUG-021 | /api/proxy/roles omits permissions; /users/current-user.role includes permissions:[] — same resource, two shapes |
Medium | Open |
| BUG-022 | Login form inputs have no name and no autocomplete — password managers break, WCAG 1.3.5 fails |
Medium | Open |
| BUG-023 | /contact "Send Us a Message" inputs have no name, no id, no aria-label |
Medium | Open |
| BUG-024 | /about has 3 <h1> tags; /contact and /faqs each have 2 — SEO + a11y |
Low | Open |
| BUG-025 | Every admin/dashboard page ships with <title>Create Next App</title> — tab labels unusable |
Medium | Open |
| BUG-026 | X-Powered-By: Next.js + x-nextjs-* headers leak backend stack on every HTML response |
Low | Open |
| BUG-027 | Any authenticated User can PUT /api/proxy/settings/:id — platform-wide fees and likely M-Pesa callback URLs are attacker-controlled |
Critical | Open |
| BUG-028 | GET /api/proxy/settings returns M-Pesa Daraja ConsumerKey / ConsumerSecret / LipaNaMpesaShortPass to every signed-in user |
Critical | Open |
| BUG-029 | BOLA: GET /api/proxy/groups/:id returns any chama's full data + members' PII (names, emails, phones) to any authenticated user |
Critical | Open |
| BUG-030 | BOLA: GET /api/proxy/transactions returns every chama's transactions (amounts, approvals, crypto hashes) to every signed-in user |
Critical | Open |
| BUG-031 | Signin reveals which emails/phones are registered (differential error + size + timing) | High | Open |
| BUG-032 | Signup leaks registration status via "Error creating user…" on existing emails |
High | Open |
| BUG-033 | Internal backend reachable from the internet at /backend/api/v1/* — doubles attack surface |
High | Open |
| BUG-034 | /api/proxy/users/request-password-reset has no per-account rate limit — mail bombing + SMS cost attack |
High | Open |
| BUG-035 | GET /api/proxy/permissions returns 201 Created with a role-list payload (routing + status bug) |
Medium | Open |
| BUG-036 | GET /api/proxy/notifications/all returns 500 Internal Server Error on every call |
Medium | Open |
| BUG-037 | Authorization failures return 400 Bad Request instead of 401/403 across signin + group + user endpoints |
Medium | Open |
| BUG-038 | Signup response contradictory status fields (isActive:false + accountStatus:"ACTIVE" + activatedAt populated) |
Medium | Open |
| BUG-039 | Signin/password-reset accept MongoDB operator objects → 500 crash (latent NoSQL injection) |
High | Open |
| BUG-040 | Any User can POST /api/proxy/roles (create roles) + PATCH /api/proxy/roles/:id (rename SuperAdmin) |
Critical | Open |
| BUG-041 | GET /api/proxy/transactions?userId=<victim> returns that user's full financial history (IDOR); no pagination |
Critical | Open |
| BUG-042 | DELETE /api/proxy/groups/:id response embeds full M-Pesa Daraja credentials in GroupSettings |
Critical | Open |
| BUG-043 | POST /api/proxy/notifications returns 500 on every call; no role guard |
Medium | Open |
| BUG-044 | Path traversal: GET /api/proxy/groups/../settings resolves to /settings, bypassing route guards — 10+ cross-path variants confirmed including M-Pesa credential exfiltration |
Critical | Open |
| BUG-045 | CORS: localhost:3000 gets duplicate ACAO: localhost, chamaconnect.io + ACAC: true, true — any localhost JS can make authenticated cross-origin requests |
High | Open |
| BUG-046 | JWT remains valid after logout — DELETE /api/auth/session only clears cookie; Bearer token confirmed working post-"logout" with no expiry |
High | Open |
| BUG-047 | Password-reset OTP brute force: 15+ attempts, zero rate limit or lockout — enables full account takeover via OTP exhaustion | High | Open |
| BUG-048 | Transaction approve leaks "Only undefined can approve" (null dereference); reject returns 500 even with reason |
Medium | Open |
| BUG-049 | GET /api/proxy/groups/types returns 500 — routing collision, "types" treated as MongoDB ObjectId |
Medium | Open |
| BUG-050 | Stored XSS: group name stores raw <script> tags without sanitization — persists in DB, returned in API responses |
High | Open |
| BUG-051 | GET /api/proxy/roles/permissions + /roles/assign return 500 (routing collision) |
Medium | Open |
| BUG-052 | /notifications/mark-all-read (wrong method), /clear (all methods), /all all return 500 |
Medium | Open |
| BUG-053 | BOLA: any user can PATCH /groups/:id/members/:memberId to change ANY chama member's role — including promoting strangers to Treasurer/ChamaAdmin |
Critical | Open |
| BUG-054 | M-Pesa callback endpoint publicly reachable, no auth/IP/signature validation — forged STK callbacks can fake contribution payments | Critical | Open |
| BUG-055 | ?limit=99999 dumps entire platform transaction ledger in one request (29 transactions, 7 chamas, 11 users) |
High | Open |
| BUG-056 | NaN / Infinity in any numeric field → 500 crash on every affected endpoint (DoS) |
Medium | Open |
| BUG-057 | withDrawalFee field casing inconsistency silently drops updates; sending canonical name nulls the field |
Medium | Open |
| BUG-058 | OTP tokens stored in plaintext and returned in current-user/signin responses — JWT holder can read & use any pending OTP without email access |
Critical | Open |
| BUG-059 | New password-reset OTP request does not invalidate prior OTPs — 34+ concurrent valid codes observed | High | Open |
| BUG-060 | Null bytes (\u0000) stored verbatim in group names — enables filter bypass and downstream truncation |
Medium | Open |
| BUG-061 | HSTS header present on API responses but absent on all HTML pages (login, homepage, admin) | High | Open |
| BUG-062 | SPF ~all softfail + DMARC p=quarantine allow spoofed @chamaconnect.io emails to reach spam |
Medium | Open |
| BUG-063 | PATCH /api/proxy/users/update-profile changes email / firstName / lastName with no password re-entry, no OTP, no re-verification — one-shot account takeover for any leaked JWT; reproduced twice on live site |
Critical | Open |
| BUG-064 | Password policy accepts "password", "password123", and eight-space strings; 6-char minimum, no blacklist, no complexity, no HIBP check |
High | Open |
| BUG-065 | Signin is email-case-sensitive + does not trim whitespace — EUGENEGABRIEL.KE@GMAIL.COM returns 400 Invalid email; silent lockout + enumeration amplifier |
High | Open |
| BUG-066 | Signin accepts ≥ 100 KB request bodies with no 413 cap — bandwidth / log-bloat amplifier for credential-stuffing |
Medium | Open |
| BUG-067 | POST /api/auth/session sets the auth_token cookie to any supplied string without verifying the JWT signature — session-fixation / XSS amplifier (SameSite=Strict mitigates direct browser-CSRF but not the design flaw) |
Medium | Open |
| BUG-068 | No CAA DNS record on chamaconnect.io — any publicly-trusted CA in the world may issue certs for the domain |
Medium | Open |
| BUG-069 | Every unmatched path (/.env, /.git/HEAD, /swagger, /api/health, …) returns a 26 KB HTML clone of the homepage — 130× bandwidth amplifier + soft-404 SEO |
Low | Open |
| BUG-070 | /users/signin accepts application/x-www-form-urlencoded — CORS-preflight bypass that becomes a full CSRF amplifier if any /api/proxy/* mutation endpoint also accepts it |
Medium | Open |
| BUG-071 | ?from, ?to, ?since, ?createdAt filters accepted but silently ignored on /transactions, /notifications, /groups — dashboard widgets silently show all-time data instead of the filtered range |
High | Open |
| BUG-072 | /api/proxy/users/admin uses ad-hoc role-name strings ("super admins" on GET vs "admins" on DELETE) — neither matches the canonical role taxonomy; 400 returned instead of 403 |
Medium | Open |
| BUG-073 | Email verification enforced at signup ("Please verify your email before signing in") but bypassable via PATCH /users/update-profile — strengthens BUG-063 ATO chain |
High | Open |
| BUG-074 | /users/update-profile uses a correct write-allowlist (roleId / isSuperadmin / accountStatus can't be hijacked) but returns 200 on unknown keys — no 400 hard-reject, so client typos silently fail to persist |
Low | Open |
| BUG-075 | Every /api/proxy/* mutation endpoint accepts application/x-www-form-urlencoded and multipart/form-data — platform-wide CSRF surface (supersedes / escalates BUG-070 from "signin only" to "all mutations") |
High | Open |
| BUG-076 | Live evidence of BUG-040: a BackendHack role created during probing still exists in the production roles table, visible to every signed-in user and surfaced in the Create-Chama wizard dropdown |
Critical | Open |
Severity (short): Critical → core job blocked or security-critical data exposure/modification; High → trust, security, or major product surface; Medium → clear UX or consistency break; Low → polish / conversion nits.
- 16 Critical — direct financial harm, secrets exposure, one-shot ATO, or persisted attacker artefacts (BUG-007 · 011 · 016 · 027 · 028 · 029 · 030 · 040 · 041 · 042 · 044 · 053 · 054 · 058 · 063 · 076).
- 27 High — auth hardening, BOLA reads, session lifetime, rate limits, stored XSS, filters silently ignored, weak password policy, HSTS, email-verification bypass, platform-wide CSRF surface, SEO metadata, other exploit amplifiers.
- 27 Medium — consistency, routing collisions, API shape, accessibility, DNS / TLS posture.
- 6 Low — metadata polish, copy typos, stack-leak headers, silent API mis-writes.
The register is backed by three distinct, reproducible evidence trails — all under recon/:
- Authenticated platform crawl —
recon/tests/explore.spec.ts+recon/tests/deep-interact.spec.tslog in with the.envcredentials, crawl every admin / chama / profile / settings route, and persist every XHR + response body torecon/artifacts/deep-*/network/. This is the source for BOLA (BUG-029/030/041/053), path traversal (BUG-044), credential leaks (BUG-028/042), roles CRUD (BUG-040), M-Pesa callback exposure (BUG-054) and most routing bugs. - Extended audit probes — three passes under
recon/tests/audit-extended*.spec.tscover the OWASP API Top-10 surface:- Pass 1 (20 probes) — static exposure, HTTP method tampering, content-type bypass, CSRF / cookies / logout, numeric edges, payload size, prototype pollution, open redirect, clickjacking, password strength, email-change / verification, cache-control, wallet-repair flag, CORS preflights, HEAD vs GET.
- Pass 2 (8 probes) — mass assignment, file upload, form-urlencoded mutations, Daraja callback forgery, emailVerified reset, WebSocket auth,
/users/admin, date-filter parsing. - Pass 3 (5 probes) — mass-assignment with valid base fields (to bypass field validation), profile-picture upload variants, form-urlencoded across all mutation endpoints, Create-Chama wizard walk-through with network capture, supply-chain fingerprints from captured bundles.
Each probe writes its raw artifact to
recon/artifacts/audit*/NN_*.jsonso every bug cites a specific capture. Mutation probes include explicit revert paths so repeat runs are idempotent.
- Static analysis — captured client bundles in
recon/probes/bundles/*.jsplus DNS / TLS data inrecon/artifacts/dns-tls-audit/were searched offline for hardcoded localhost URLs (BUG-011), exposed source maps, dead API paths,/superadmin/dashboardreferences, SPF / DMARC / CAA posture (BUG-062 / BUG-068) and similar.
Re-run any pass end-to-end with:
cd recon && npm install && npx playwright install chromium
npm test # explore + deep-interact
npx playwright test tests/audit-extended.spec.ts # pass 1 (20 probes)
npx playwright test tests/audit-extended-2.spec.ts # pass 2 (8 probes)
npx playwright test tests/audit-extended-3.spec.ts # pass 3 (5 probes)Pass-2 probes that mutate data (21 mass-assignment, 22 file upload, 23 form-urlencoded mutations, 25 emailVerified reset) gracefully skip unless a fresh probe account with a completed email-verification OTP is available. Pass-3 mutation probes run against Eugene's account and include explicit reverts — profile picture, first/last name, and email are restored to their original values at the end of each test so repeat runs are idempotent and side-effect-free against the live platform. One-shot side-effects from earlier probe runs (a handful of @probe.local accounts, +25470… phone slots, and the BackendHack role) are listed in BUG-064, BUG-073, and BUG-076 for MUIAA to purge.
Login creds live in ./.env at the repo root (gitignored). Copy from .env.example. The Playwright recon logs in, crawls every authenticated route, screenshots each, records every XHR, and dumps JSON to recon/artifacts/<timestamp>/:
cd recon
npm install
npx playwright install chromium
npm testEvery run produces:
screenshots/— full-page screenshots of every route visitedhtml/— full rendered HTML of every routenetwork/requests.json— every XHR / fetch / document with request + response bodysummary.json— one-line overview
Large artifact trees stay gitignored; retain representative runs for submission evidence if needed.
See CONTRIBUTING.md for local setup in chamapay/ and recon/, commands to run before a PR (lint, typecheck, test, build), and how to file new bugs under bugs/.
MIT. See LICENSE.