The Payment Method Called Nothing — JavaScript Bug Hunt

Modelled on the Uber free-rides bug that security researcher Anand Prakash disclosed in 2017 after reporting it through Uber's bug bounty programme.

  • Language: JavaScript
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Payments, Validation
  • Modelled on: Uber · 2017
  • Visible tests: a ride on a verified card is dispatched and charged; an unsupported payment type is refused before dispatch
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the Uber free-rides bug that security researcher Anand Prakash disclosed in 2017 after reporting it through Uber's bug bounty programme. By setting the payment method on a ride request to an invalid value, he could take rides that were never charged. The request was accepted and the car dispatched; the payment method was only exercised at the end of the trip, when it could no longer be charged. Uber fixed it after the report.

dispatch.js is a reconstruction: requestRide dispatches a car for any payment method id, and completeRide records the charge failure and completes the trip anyway.

Fix requestRide so an unchargeable payment method is refused before a car is dispatched.

Bug report

BUG-RIDE-FREE · Priority: Critical (revenue) · Reported by: bug bounty

requestRide(account, request, trips):

  • look up account.paymentMethods[request.paymentMethodId]
  • if payments.isChargeable(method) is false (missing id, unknown id, unsupported type, unverified method), return { ok: false, error: "invalid_payment_method" } and push NO trip
  • otherwise push and return { ok: true, trip } with the trip as today (id "trip-<n>", status "dispatched")

completeRide charges the fare to the ledger exactly once for a valid trip.

Observed: a request with paymentMethodId pointing at type "xyz" is dispatched and completed; the ledger has no charge for it.

Logs

[dispatch] trip-8812 dispatched rider=rider-7 payment=pm_x
[billing] trip-8812 charge failed: payment method cannot be charged
[billing] trip-8812 completed fare=1250 collected=0

The code as shipped

src/rides/dispatch.js (editable)

var payments = require("./payments");

exports.requestRide = function (account, request, trips) {
  var trip = {
    id: "trip-" + (trips.length + 1),
    rider: account.id,
    paymentMethodId: request.paymentMethodId,
    fare: request.fare,
    status: "dispatched"
  };
  trips.push(trip);
  return { ok: true, trip: trip };
};

exports.completeRide = function (account, trip, ledger) {
  var method = account.paymentMethods[trip.paymentMethodId];
  try {
    payments.charge(method, trip.fare, ledger);
  } catch (e) {
    trip.chargeError = e.message;
  }
  trip.status = "completed";
  return trip;
};

Read-only context: src/rides/payments.js.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.