From 1d994c93021f6522114484a4c5c2b50427aeffb1 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Tue, 15 Sep 2026 13:19:05 +0800 Subject: [PATCH] fix: validate attendance checkin payload --- internal/httpapi/api.go | 82 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/internal/httpapi/api.go b/internal/httpapi/api.go index cc9d740..cadc0e0 100644 --- a/internal/httpapi/api.go +++ b/internal/httpapi/api.go @@ -248,31 +248,65 @@ func (a *API) attendanceCheckins(w http.ResponseWriter, r *http.Request, p *auth return } var body struct { - Date string `json:"date"` - Action string `json:"action"` - RuleID string `json:"rule_id"` - ShiftID string `json:"shift_id"` - ClientTime string `json:"client_time"` - Location string `json:"location"` - Wifi string `json:"wifi"` - Device string `json:"device"` + Date string `json:"date"` + Action string `json:"action"` + RuleID string `json:"rule_id"` + ShiftID string `json:"shift_id"` + ClientTime string `json:"client_time"` + Location json.RawMessage `json:"location"` + Wifi json.RawMessage `json:"wifi"` + Device json.RawMessage `json:"device"` } if !decodeBody(w, r, &body) { return } + action := strings.TrimSpace(body.Action) + if action == "" || len(action) > 32 { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "action"}) + return + } + ruleID := strings.TrimSpace(body.RuleID) + if len(ruleID) > 64 { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "rule_id"}) + return + } + shiftID := strings.TrimSpace(body.ShiftID) + if shiftID == "" { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "shift_id"}) + return + } date, err := parseDate(body.Date) if err != nil { writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "date"}) return } + if maxShiftIDLength := maxAttendanceShiftIDLength(p.UserID, date); len(shiftID) > maxShiftIDLength { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "shift_id", "max_length": maxShiftIDLength}) + return + } clientTime, err := parseTime(body.ClientTime) if err != nil { writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "client_time"}) return } + location, err := normalizeJSONObject(body.Location) + if err != nil { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "location"}) + return + } + wifi, err := normalizeJSONObject(body.Wifi) + if err != nil { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "wifi"}) + return + } + device, err := normalizeJSONObject(body.Device) + if err != nil { + writeError(w, r, http.StatusBadRequest, 30000, reasonMalformedRequest, "malformed request", map[string]any{"field": "device"}) + return + } record, checkin, err := a.store.CreateAttendanceCheckin(r.Context(), p.UserID, "attc_"+randomToken(12), store.AttendanceCheckinRequest{ - Date: date, Action: strings.TrimSpace(body.Action), RuleID: strings.TrimSpace(body.RuleID), ShiftID: strings.TrimSpace(body.ShiftID), - ClientTime: clientTime, Location: strings.TrimSpace(body.Location), Wifi: strings.TrimSpace(body.Wifi), Device: strings.TrimSpace(body.Device), IdempotencyKey: idempotencyKey, + Date: date, Action: action, RuleID: ruleID, ShiftID: shiftID, + ClientTime: clientTime, Location: location, Wifi: wifi, Device: device, IdempotencyKey: idempotencyKey, }) if err != nil { writeStoreError(w, r, err) @@ -957,6 +991,34 @@ func parseTime(value string) (time.Time, error) { return time.ParseInLocation("2006-01-02 15:04:05", value, time.Local) } +func maxAttendanceShiftIDLength(userID int64, date time.Time) int { + prefix := fmt.Sprintf("att_%d_%s_", userID, date.Format("20060102")) + return 64 - len(prefix) +} + +func normalizeJSONObject(raw json.RawMessage) (string, error) { + if len(raw) == 0 || strings.TrimSpace(string(raw)) == "" || string(raw) == "null" { + return "{}", nil + } + var text string + if err := json.Unmarshal(raw, &text); err == nil { + text = strings.TrimSpace(text) + if text == "" || text == "null" { + return "{}", nil + } + raw = json.RawMessage(text) + } + var value map[string]any + if err := json.Unmarshal(raw, &value); err != nil { + return "", err + } + normalized, err := json.Marshal(value) + if err != nil { + return "", err + } + return string(normalized), nil +} + func randomToken(size int) string { buf := make([]byte, size) if _, err := rand.Read(buf); err != nil {