The first Seraphim Scan case study: an assessment of an application its owner also built and runs. We show what was found, how we rate it, what changed, and, just as important, what was not done.
Case study 01 · September 2026 · Findings are summarized in public-safe language; no exploit steps, secrets, or internal identifiers are published.
Assessment summary
Who, what, and how
Assessor
Seraphim Scan, an Ascend Maui initiative
Target
A regional airport ride-booking platform (rider and driver apps, web API, card payments), owned and operated by the same principal. This is a self-engagement.
Method
Reconnaissance, code review, and remediation with verification. See the phase table below for what was and was not done.
Scope
Self-assessment of the owner's own application. Scope is limited to the application's own code and configuration.
Report status
No formal engagement memo or scan report has been issued for this self-assessment. This page summarizes documented code-review findings and the application's remediation log.
Method, honestly
The three phases: what actually happened
Seraphim Scan's method has three phases. For this engagement they were not all performed in full, and we say so.
Phase 01
Recon
Performed (observational)
An observational inventory of the application: what it does, its tech stack, whether it handles user data and payments, and a sketch of its surface (accounts, public API routes, payments, an admin area). No probing was involved.
Phase 02
Scoped Assessment
Not performed
No active scanning or exploitation was carried out. In its place, the assessor did a read-only review of the application's source code, which is where the code-review findings below come from.
Phase 03
Remediation and Retest
Performed, with verification
Fixes were written and merged. Where the remediation log records a production change or a verification check, it is stated with the finding.
Findings
8 findings, all documented and addressed
Severities are proposed assessor ratings, not the output of a formal scoring process.
High 1
Medium 4
Low 3
F-01Medium · proposed
Scheduled-maintenance endpoint trusted a platform header
Reachable without credentials, but the action is bounded: it only expires unpaid ride holds that are already past their time limit.
What it was
An endpoint that expires unpaid ride holds is meant to be called only by a scheduler holding a shared secret. In production that secret had never been set, so the endpoint fell back to accepting a platform-supplied request header as proof of the caller. The same route also compared credentials with a plain equality check, could expire the same payment session twice when sweeps overlapped, and could echo internal error text.
Before
No shared secret configured; a header that any client can also send was enough to be treated as the scheduler.
After
A random secret is set in production and stored in the database vault. The scheduler presents it as a bearer credential, compared in constant time; the header fallback only applies when no secret is configured. Overlapping sweeps are claimed atomically and errors are generic.
Suggested fix
Require a high-entropy shared secret in every environment that serves the route; compare it in constant time; fail closed when it is missing; keep error detail server-side.
Status: Fixed. Verified: a request with the platform header but no credential returned 401, and a wrong credential returned 401.
F-02High · proposed
Pickup date and time interpreted in the server's timezone
Directly distorts what riders are charged (surge windows) and when dispatch believes a pickup occurs; ranked highest in the code review.
What it was
A rider's chosen pickup date and time were parsed without a timezone, so on a server running in UTC an afternoon pickup was evaluated as a morning one. Surge windows were then evaluated against the wrong moment, mis-pricing airport-rush, weekend, and game-day rides.
Before
Wall-clock time was read in the server's local zone: 2:00 PM local was stored as 2:00 PM UTC, roughly four hours early.
After
Civil date and time are read as the product's local wall clock and converted to one absolute instant. The same input now yields the same instant regardless of server timezone. Ambiguous or non-existent daylight-saving hours are handled by an explicit policy (first occurrence; skipped hour rejected). Fare formulas were not changed.
Suggested fix
Never parse timezone-naive strings on the server; convert from the product timezone explicitly and test under a UTC runtime.
Status: Fixed and merged, with tests covering timezone independence and daylight-saving edges.
F-03Medium · proposed
Scheduled trips always recorded a single passenger
Party size never reached capacity-aware logic on the scheduling path; impact depends on how dispatch uses the count.
What it was
The scheduling path hard-coded one passenger for every trip, including weekend party rides, so any downstream check that relies on the stored passenger count treated every party as a single rider.
Before
Passenger count was a constant 1 regardless of the request.
After
The requested party size is read from the request (defaulting to 1 when absent or invalid) and stored on the trip. Fare inputs are unchanged.
Suggested fix
Persist a validated party size; enforce a seat limit as a product decision informed by vehicle capacity.
Status: Fixed and merged (persists the requested size). Any hard seat cap remains a product decision.
F-04Low · proposed
Failed audit-log writes were silently ignored
Weakens auditability and troubleshooting; no direct exposure of data or funds.
What it was
After a trip was created, the write of its audit event was fire-and-forget: a failure produced no log line and no error, and the request still reported success, so the event history could drift from reality unnoticed.
Before
Event insert result never inspected; request returned success either way.
After
Event writes go through one helper that always logs failures and returns the error. Scheduling and settlement report a distinct error code; the mid-ride cancellation path keeps its success response (money already moved) but flags the event failure.
Suggested fix
Check every audit-write result, log loudly, and choose per path whether failure should block or only flag.
Status: Fixed and merged, with tests for the failure path.
F-05Low · proposed
Student-discount eligibility disagreed between quote and display
Blank-tier edge case; risk is a price shown that differs from the price charged.
What it was
Two helpers decided student-discount eligibility differently for a blank rider tier: one granted the discount and the other did not, so the displayed price and the checkout price could differ.
Before
A blank tier received a discount in the display path but not in the checkout path.
After
Both paths use the same rule: only an omitted or explicitly standard tier qualifies. Multipliers and formulas are unchanged.
Suggested fix
Use one shared eligibility predicate for every code path that prices a ride.
Status: Fixed and merged, with tests asserting both paths agree.
F-06Medium · proposed
Referral payout ledger could record the same trip twice
Financial integrity: a single completed trip could be paid out more than once.
What it was
Settling a shared ride inserted a payout-ledger row on every run and the table had no uniqueness rule, so re-running settlement could create duplicate payout entries for one trip and one referral code.
Before
Plain insert; no unique key on trip and referral code.
After
A unique index on the pair was added and settlement uses an insert-if-absent write that reports "already recorded" instead of duplicating. A read-only check of production data found no existing duplicates before the index was added.
Suggested fix
Enforce idempotency in the database, not only in application code.
Status: Fixed per the remediation log.
F-07Medium · proposed
Charge idempotency key included the amount, allowing a second payment intent
Could produce a duplicate charge attempt when a fare moved during card authentication.
What it was
Idempotency keys for several charge types embedded the fare amount. If the fare changed between retries, the key changed and a second payment intent could be created instead of reusing the open one.
Before
Key included the charge amount, so a moved fare produced a new key.
After
Keys no longer contain the amount. They carry a generation marker that changes only after a successful charge, and an existing payment intent is retrieved and reused (or replaced only if canceled).
Suggested fix
Derive idempotency keys from stable identifiers plus a generation counter, and look up existing intents before creating new ones.
Status: Fixed per the remediation log.
F-08Low · proposed
Two row-level security policies referenced each other
Availability defect (sign-in queries errored); the log documents no data exposure.
What it was
A policy on one table queried a second table whose own policy queried the first, producing an infinite-recursion error that broke sign-in for new sessions.
Before
Circular policy dependency between the profile and trip tables.
After
The cross-table check was moved into a narrowly scoped privileged helper function, breaking the cycle. An audit of all public-schema policies found no other cycles.
Suggested fix
Avoid policy-to-policy cycles; put shared role checks in a locked-down helper and audit policies for recursion.
Status: Fixed and applied to the live database; verified in a rolled-back transaction.
Remediation
Remediation: all 8 findings fixed
Status as of 29 September 2026. All 8 findings above are fixed in the application's current source, and none is pending. We re-read the current code for each one and ran the application's automated test suite (1,757 tests, passing under both a UTC and a US-Eastern runtime). This was a source and test check only: no requests were sent to the running application, and no active assessment was performed. Where a production check is mentioned, it comes from the application's own remediation log and was not re-run.
F-01Medium · proposed
Scheduled-maintenance endpoint trusted a platform header
What was found
With no shared secret configured in production, the endpoint accepted a spoofable platform header as proof of the scheduler.
What changed
Constant-time bearer comparison; the header is honored only on the hosting platform and only when no usable secret exists; overlapping sweeps are claimed atomically; generic error responses. A random secret was set in production and the scheduler wired to present it.
Checked 29 Sep 2026: Code and unit tests confirmed on current main. The production 401 checks are from the application's remediation log, not re-run.
F-02High · proposed
Pickup date and time interpreted in the server's timezone
What was found
Timezone-naive pickup times were parsed in the server zone, shifting surge windows by hours.
What changed
Date and time are read as the product's local wall clock and converted to a single instant; daylight-saving edge cases have an explicit policy. Fare formulas unchanged.
Checked 29 Sep 2026: Code confirmed on current main; tests pass under both a UTC and a US-Eastern runtime.
F-03Medium · proposed
Scheduled trips always recorded a single passenger
What was found
The scheduling path stored one passenger regardless of party size.
What changed
The requested party size is validated (default 1) and stored on the trip. A hard seat cap was not added and remains a product decision.
Checked 29 Sep 2026: Code and tests confirmed on current main.
F-04Low · proposed
Failed audit-log writes were silently ignored
What was found
Trip event inserts were fire-and-forget, so a failed write left no log line or error.
What changed
Server-side event writes go through one helper that logs and returns the error; scheduling and settlement return a distinct error code. Two client-side write sites (driver accept, driver desk) also log and surface failures.
Checked 29 Sep 2026: Code and tests confirmed on current main.
F-05Low · proposed
Student-discount eligibility disagreed between quote and display
What was found
A blank rider tier got the discount in one path and not in the other.
What changed
Both paths apply the same rule: only an omitted or standard tier qualifies.
Checked 29 Sep 2026: Code and tests confirmed on current main.
F-06Medium · proposed
Referral payout ledger could record the same trip twice
What was found
Settlement inserted a ledger row on every run with no uniqueness rule.
What changed
A unique index on trip and referral code, and an insert-if-absent write that reports "already recorded".
Checked 29 Sep 2026: Migration, settlement code, and tests confirmed on current main. The absence of pre-existing duplicates in production is from the remediation log, not re-run.
F-07Medium · proposed
Charge idempotency key included the amount
What was found
A fare that moved during card authentication produced a new key and a second payment intent.
What changed
Keys are built from stable identifiers plus a generation marker that changes only after a successful charge; the existing payment intent is retrieved and reused.
Checked 29 Sep 2026: Key-building code and tests confirmed on current main. Behavior against the live payment processor was not exercised.
F-08Low · proposed
Two row-level security policies referenced each other
What was found
A circular policy dependency between the profile and trip tables broke sign-in queries.
What changed
The cross-table check moved into a narrowly scoped privileged helper function.
Checked 29 Sep 2026: Migration present on current main. That it is applied to the live database is from the remediation log, not re-checked.
Limits of this write-up
What this case study does not claim
It does not describe an active penetration test, vulnerability scan, or exploitation.
Findings come from a read-only code review and the application's own remediation log; the follow-up check was also limited to source and tests; they are not an exhaustive list of the application's weaknesses.
Ratings are proposed by the assessor. Because the assessor and owner are the same principal, this is a self-assessment and not independent assurance.
Want an assessment of your own?
Tell us what you need assessed. We'll reply with scope questions and next steps.