fix(payments,portfolio,scheduling): add ID validation hardening

Add validators.IsValidID() checks on URL param IDs to return 404 instead of 400 for invalid IDs. Add offset cap and query length limit in portfolio images handler.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-05-31 18:49:16 +01:00
co-authored by Sisyphus
parent 1d8247e655
commit b1847b4cfc
3 changed files with 24 additions and 15 deletions
+13 -12
View File
@@ -2,6 +2,7 @@ package payments
import ( import (
"crussell/internal/square" "crussell/internal/square"
"crussell/internal/validators"
"crussell/mw" "crussell/mw"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -87,8 +88,8 @@ type PaymentSummaryResponse struct {
func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) { func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id") bookingID := chi.URLParam(r, "id")
if bookingID == "" { if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking ID is required", http.StatusBadRequest) http.Error(w, "Booking not found", http.StatusNotFound)
return return
} }
@@ -260,8 +261,8 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
func CreateBookingPayment(w http.ResponseWriter, r *http.Request) { func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id") bookingID := chi.URLParam(r, "id")
if bookingID == "" { if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking ID is required", http.StatusBadRequest) http.Error(w, "Booking not found", http.StatusNotFound)
return return
} }
@@ -459,8 +460,8 @@ func GetUserPaymentMethods(w http.ResponseWriter, r *http.Request) {
func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) { func DeletePaymentMethod(w http.ResponseWriter, r *http.Request) {
cardID := chi.URLParam(r, "id") cardID := chi.URLParam(r, "id")
if cardID == "" { if cardID == "" || !validators.IsValidID(cardID) {
http.Error(w, "Card ID is required", http.StatusBadRequest) http.Error(w, "Payment method not found", http.StatusNotFound)
return return
} }
@@ -524,8 +525,8 @@ func CreatePaymentMethod(w http.ResponseWriter, r *http.Request) {
func RefundPayment(w http.ResponseWriter, r *http.Request) { func RefundPayment(w http.ResponseWriter, r *http.Request) {
paymentID := chi.URLParam(r, "payment_id") paymentID := chi.URLParam(r, "payment_id")
if paymentID == "" { if paymentID == "" || !validators.IsValidID(paymentID) {
http.Error(w, "Payment ID is required", http.StatusBadRequest) http.Error(w, "Payment not found", http.StatusNotFound)
return return
} }
@@ -640,8 +641,8 @@ func RefundPayment(w http.ResponseWriter, r *http.Request) {
func CreateTipPayment(w http.ResponseWriter, r *http.Request) { func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id") bookingID := chi.URLParam(r, "id")
if bookingID == "" { if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking ID is required", http.StatusBadRequest) http.Error(w, "Booking not found", http.StatusNotFound)
return return
} }
@@ -753,8 +754,8 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) { func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id") bookingID := chi.URLParam(r, "id")
if bookingID == "" { if bookingID == "" || !validators.IsValidID(bookingID) {
http.Error(w, "Booking ID is required", http.StatusBadRequest) http.Error(w, "Booking not found", http.StatusNotFound)
return return
} }
+9 -1
View File
@@ -6,6 +6,7 @@ import (
"crussell/db" "crussell/db"
"crussell/internal/images" "crussell/internal/images"
"crussell/internal/s3" "crussell/internal/s3"
"crussell/internal/validators"
"crussell/mw" "crussell/mw"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -114,7 +115,7 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
} }
} }
if o := r.URL.Query().Get("offset"); o != "" { if o := r.URL.Query().Get("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 && parsed <= 10000 {
offset = parsed offset = parsed
} }
} }
@@ -264,6 +265,9 @@ func ListImages(w http.ResponseWriter, r *http.Request) {
func ListTags(w http.ResponseWriter, r *http.Request) { func ListTags(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q") q := r.URL.Query().Get("q")
if len(q) > 100 {
q = q[:100]
}
var query string var query string
var args []interface{} var args []interface{}
@@ -694,6 +698,10 @@ func DeleteImage(w http.ResponseWriter, r *http.Request) {
} }
imageID := chi.URLParam(r, "id") imageID := chi.URLParam(r, "id")
if imageID == "" || !validators.IsValidID(imageID) {
http.Error(w, "Image not found", http.StatusNotFound)
return
}
var url, thumbURL string var url, thumbURL string
err := db.DB.QueryRow(r.Context(), ` err := db.DB.QueryRow(r.Context(), `
+2 -2
View File
@@ -169,8 +169,8 @@ func CreateTimeBlocker(w http.ResponseWriter, r *http.Request) {
// DELETE /api/admin/time-blockers/{id} // DELETE /api/admin/time-blockers/{id}
func DeleteTimeBlocker(w http.ResponseWriter, r *http.Request) { func DeleteTimeBlocker(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") id := chi.URLParam(r, "id")
if id == "" { if id == "" || !validators.IsValidID(id) {
http.Error(w, "missing id parameter", http.StatusBadRequest) http.Error(w, "time blocker not found", http.StatusNotFound)
return return
} }