Full codebase audit — 5 September 2026
928 files, ~180,000 lines: backend, admin, both mobile apps, sales site.
Run because four white-label bugs had just been found by eye, all of one
class, all in files nobody had happened to open. That is not a method. The
scanners behind this report live in tools/audit/ and are
re-runnable.
264 raw hits → 6 real defects. The rest are documented false positives; the tooling README lists the known-good patterns so the next run is faster to triage.
Fixed
1. Any signed-in user could read any ride — 3 routes · security
GET /rides/:id, /rides/:id/timeline and /rides/:id/candidates sat behind
JwtAuthGuard alone and read only the id from the URL. No handler looked at
who was asking.
Exposed: both parties' user ids, pickup and dropoff addresses and
coordinates, the fare, the full status history — and, on /candidates, the
list of nearby drivers with their positions.
Reachable in practice, not in theory: a driver is handed the id of every ride they are offered, and declining does not forget it. A driver could decline a trip and then keep reading it, watching where that passenger was actually taken.
The proof it was an oversight rather than a design choice: the same
candidatesFor() call is exposed on the dispatcher controller behind
@RequirePermissions('ride.assign'). One controller guarded it; the other
handed it to anyone.
Fixed with RidesService.getForParty() — rider, driver, or an operator
holding ride.view_all. /candidates now requires the same permission its
twin always did (no client ever called this copy). 13 regression tests in
ride-access.spec.ts, including one asserting no @Get(':id…') handler reads
an id without also reading the viewer — the check that would have caught all
three.
A sweep of all 166 id-bearing routes across 37 controllers found no others.
2. Two dispatch timers with no rejection boundary — latent crash
setTimeout(() => { this.onTimeout(rideId, driverId); }, OFFER_TTL_MS);
setTimeout(() => { void this.runAutoAccept(...); }, delayMs);
onTimeout is async and reaches the database. void marks a promise as
deliberately unawaited; it does not handle a rejection. Neither callback had
a .catch(), and the process had no unhandledRejection handler — where Node's
default is to terminate.
Honest severity: not a live crash. Every helper down that tree currently guards itself, so nothing is known to reject today. But the safety of the busiest path in the product rested on all ~15 of them continuing to do so forever, with a timer firing every 15 seconds of every dispatch to collect on the first one that stopped.
Fixed: explicit .catch() + logger.error on both callbacks, plus a
process-level unhandledRejection net in main.ts that logs loudly and reports
to Sentry instead of ending the process. uncaughtException deliberately left
alone — a synchronous throw really can leave state inconsistent, and exiting is
the safer answer there.
3. Every phone field in the admin showed an Indian number
placeholder="+919800000000" in Drivers, Riders, Team and Settings — on a
product sold into 37 markets. An operator in Riyadh was shown an Indian number
as the format to follow.
The preset pack has carried dial and phonePlaceholder per country all along;
nothing served them. Fixed: both now come through /branding and
useInstallation().phoneExample.
4. The browser tab still said "RideKit"
app/layout.tsx exports a static metadata title, resolved at build time — so
every buyer's admin announced itself in the tab, the bookmark and browser
history as RideKit — Admin. The one piece of chrome an operator looks at all
day.
Fixed: corrected client-side once /branding answers; the static export
stays as the pre-hydration fallback.
5–6. Two white-label leaks in the driver app
- The background-location foreground-service notification used
getCachedBrandName()on one line and a hardcoded#4f46e5on the next — a buyer's driver ran an entire trip with our indigo in their status bar. - The notification channel LED colour was literal, while the rider app derived it. The two apps had drifted.
Both were caused by the driver app mirroring only brandName for non-hook
callers and never the colour. Fixed by remembering them together through one
rememberBrand(), so a third cannot appear.
Found, not fixed — needs your decision
A. A failed request renders as "you have no data" — ~80 sites
.catch(() => setItems([])) across both mobile apps. A network failure becomes
an empty list, indistinguishable from genuinely having nothing. The rider sees
"No saved places" when their addresses are fine, or an empty chat thread when
the driver did write.
This is a known-real class here — the admin Providers screen had exactly it, and the code comment there calls it "the most expensive lie on this screen".
Not fixed because it is not a find-and-replace: each screen needs an error state and a retry, which is a design decision per screen. Roughly 20 of the 80 are harmful (lists a user would believe); the rest are decorative fetches where empty is honest. Worth scoping as its own piece of work.
B. TODO markers on public legal pages
site/src/app/legal/terms/page.tsx renders TODO(owner): legal entity and
TODO: confirm to visitors. Not a code bug — it needs your legal entity name
and a decision on support scope.
C. Ride-category tile colours
countries.ts gives Ride/Bike/Auto three fixed hues, the first of which is our
teal. They read as category colours rather than brand, but a red-branded install
shows a teal "Ride" tile. Your locked design direction, so not mine to override.
Checked and clean
| Result | |
|---|---|
| Secrets in source | None. 3 hits, all test fixtures |
| Money as float | 0 real defects in 19 hits — all display formatting, documented rounding, or safe conversion. Verified across 100M values that Number(paise)/100 always stringifies to a clean ≤2dp decimal, so GST e-invoices are exact |
IDOR beyond /rides |
None across 166 id-bearing routes |
| Public controllers | 2, both correct: /health and the Play-Store-mandated account-deletion form |
| Operator route permissions | All guarded, most at class level |
Verification
Backend 1180 · admin 355 · rider 456 · driver 237 — 2,228 tests, four clean typechecks. 13 of those tests are new and pin the security fix.