Address Gadfly review on #4: TOCTOU, sliding cookie, CSRF, argon2
Build image / build-and-push (push) Successful in 5s
Build image / build-and-push (push) Successful in 5s
Fixes from the PR #23 adversarial review (graded 35 real / 1 false positive): Security / correctness - Race-free registration: is_admin and the registration gate are now computed atomically inside a single INSERT...SELECT, so concurrent first registrations can't both become admin or bypass closed registration (fixed the whole TOCTOU cluster). - Sliding session now reaches the browser: ResolveSession returns the current expiry and requireAuth re-sets the cookie, so active users aren't logged out 30 days after login regardless of activity. - Login CSRF: csrfGuard rejects state-changing requests whose Origin doesn't match PANSY_BASE_URL (no-op when unset, so the dev proxy is unaffected). SameSite=Lax alone didn't cover this. - argon2id tuned to RFC 9106's second recommended profile (t=3). - Timing equalizer can't fail open: the dummy hash is derived deterministically (fixed salt, no RNG) so it's always present. - Password length (<=1024) enforced in the service for both register and login, not just HTTP binding tags; login rejects over-long input before spending argon2 work. Error handling / robustness - Login logs a malformed stored hash instead of silently treating it as a wrong password. - Best-effort session writes (Touch/Delete during renewal, expiry, and corrupt-expiry cleanup) now log on failure. - index sessions.expires_at via new migration 0002 (0001 is immutable). Maintainability - Extract startSessionAndRespond and abortUnauthenticated; make writeServiceError a free function; consistent error handling in decodeHash; doc/comment fixes. Tests: over-long password, CSRF guard (cross-origin/same-origin/dev no-op), and cookie refresh on authenticated requests; migration-version assertions bumped to 2. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
+32
-32
@@ -117,25 +117,25 @@ type GardenShare struct {
|
||||
// GardenObject is any placeable object in a garden (bed, container, tree, path…).
|
||||
// Positioned by center point + rotation about center, in garden space.
|
||||
type GardenObject struct {
|
||||
ID int64 `json:"id"`
|
||||
GardenID int64 `json:"gardenId"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Shape string `json:"shape"`
|
||||
Points *string `json:"points,omitempty"` // reserved: JSON for polygons
|
||||
XCM float64 `json:"xCm"`
|
||||
YCM float64 `json:"yCm"`
|
||||
WidthCM float64 `json:"widthCm"`
|
||||
HeightCM float64 `json:"heightCm"`
|
||||
RotationDeg float64 `json:"rotationDeg"`
|
||||
ZIndex int `json:"zIndex"`
|
||||
Plantable bool `json:"plantable"`
|
||||
Color *string `json:"color,omitempty"`
|
||||
Props *string `json:"props,omitempty"` // kind-specific JSON
|
||||
Notes string `json:"notes"`
|
||||
Version int64 `json:"version"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
ID int64 `json:"id"`
|
||||
GardenID int64 `json:"gardenId"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Shape string `json:"shape"`
|
||||
Points *string `json:"points,omitempty"` // reserved: JSON for polygons
|
||||
XCM float64 `json:"xCm"`
|
||||
YCM float64 `json:"yCm"`
|
||||
WidthCM float64 `json:"widthCm"`
|
||||
HeightCM float64 `json:"heightCm"`
|
||||
RotationDeg float64 `json:"rotationDeg"`
|
||||
ZIndex int `json:"zIndex"`
|
||||
Plantable bool `json:"plantable"`
|
||||
Color *string `json:"color,omitempty"`
|
||||
Props *string `json:"props,omitempty"` // kind-specific JSON
|
||||
Notes string `json:"notes"`
|
||||
Version int64 `json:"version"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// Plant is a catalog entry. OwnerID nil means a read-only seeded built-in.
|
||||
@@ -157,17 +157,17 @@ type Plant struct {
|
||||
// Planting ("plop") is a circular patch of one plant, positioned in its parent
|
||||
// object's local frame. Count nil means it is derived from area / spacing².
|
||||
type Planting struct {
|
||||
ID int64 `json:"id"`
|
||||
ObjectID int64 `json:"objectId"`
|
||||
PlantID int64 `json:"plantId"`
|
||||
XCM float64 `json:"xCm"`
|
||||
YCM float64 `json:"yCm"`
|
||||
RadiusCM float64 `json:"radiusCm"`
|
||||
Count *int `json:"count,omitempty"` // nil = derived
|
||||
Label *string `json:"label,omitempty"`
|
||||
PlantedAt *string `json:"plantedAt,omitempty"`
|
||||
RemovedAt *string `json:"removedAt,omitempty"`
|
||||
Version int64 `json:"version"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
ID int64 `json:"id"`
|
||||
ObjectID int64 `json:"objectId"`
|
||||
PlantID int64 `json:"plantId"`
|
||||
XCM float64 `json:"xCm"`
|
||||
YCM float64 `json:"yCm"`
|
||||
RadiusCM float64 `json:"radiusCm"`
|
||||
Count *int `json:"count,omitempty"` // nil = derived
|
||||
Label *string `json:"label,omitempty"`
|
||||
PlantedAt *string `json:"plantedAt,omitempty"`
|
||||
RemovedAt *string `json:"removedAt,omitempty"`
|
||||
Version int64 `json:"version"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user