fix: customer PIN-reset takeover, booking-quote and consignment-log IDORs
- POST /customer/reset-pin was unauthenticated and overwrote a customer's PIN
given only their phone number — which is the login identifier, not a secret —
so reset-pin followed by verify-pin took over any customer account. Exactly
the miler flaw fixed in fd7cf3e, on the B2C side. It now requires the account's
registered email to have been verified through the existing
send-email-otp/verify-email-otp flow; the verification is recorded in Redis
for 10 minutes and consumed on use, so one verification authorises one reset.
Accounts with no email on file are directed to support rather than left open.
- GET /customer/bookings/:id/price had no ownership check, unlike every other
customer booking route, so any signed-in customer could read the price quoted
on anyone else's booking by walking the id.
- GET /miler/consignments/userlogs/:userid took the rider from the URL and never
compared it to the caller, letting any miler read another miler's movement
history.
Verified as already correct while sweeping: miler assignment and booking-flow
handlers all scope by mileruserid/assignedmileruserid, customer booking detail
and cancel scope by appcustomerid, /internal sits behind InternalKeyAuth, and
CreateHubStaffAccount already refuses non-Doormile staff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -189,6 +189,19 @@ func ResetCustomerPin(c *fiber.Ctx) error {
|
||||
return utils.NotFound(c, "customer not found")
|
||||
}
|
||||
|
||||
// Proof of identity is required before overwriting a login credential.
|
||||
// Without it this endpoint reset any customer's PIN from their phone number
|
||||
// alone — and phone numbers are the login identifier, not a secret — so
|
||||
// reset-pin followed by verify-pin was a complete account takeover.
|
||||
// The caller must first pass /customer/send-email-otp and
|
||||
// /customer/verify-email-otp for this account's registered address.
|
||||
if customer.Email == "" {
|
||||
return utils.Forbidden(c, "this account has no registered email to verify against — contact support to reset the PIN")
|
||||
}
|
||||
if !ConsumeEmailVerification(customer.Email) {
|
||||
return utils.Forbidden(c, "verify your registered email first via /customer/send-email-otp and /customer/verify-email-otp")
|
||||
}
|
||||
|
||||
pinHash, err := utils.HashPassword(req.NewPin)
|
||||
if err != nil {
|
||||
return utils.Internal(c, "failed to process PIN reset")
|
||||
@@ -620,11 +633,22 @@ func CancelCustomerBooking(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func GetCustomerBookingQuote(c *fiber.Ctx) error {
|
||||
customerID := c.Locals("userid").(int)
|
||||
bookingID, err := strconv.Atoi(c.Params("bookingid"))
|
||||
if err != nil {
|
||||
return utils.BadRequest(c, "invalid booking ID")
|
||||
}
|
||||
|
||||
// Ownership is checked here as it is on the other booking routes — without
|
||||
// it any signed-in customer could read the price quoted on anyone else's
|
||||
// booking just by walking the id.
|
||||
var booking models.PickupBooking
|
||||
if err := db.DB.Select("bookingid").
|
||||
Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).
|
||||
First(&booking).Error; err != nil {
|
||||
return utils.NotFound(c, "booking not found")
|
||||
}
|
||||
|
||||
var serviceOpt models.BookingServiceOption
|
||||
if err := db.DB.Where("bookingid = ?", bookingID).Order("createdat DESC").First(&serviceOpt).Error; err != nil {
|
||||
return utils.NotFound(c, "price quote not found for this booking")
|
||||
|
||||
Reference in New Issue
Block a user