Case studies

Self-engagement: airport ride platform

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.