Skip to content

April 14: Hardening Spree

Thirteen-PR security and correctness blitz — auth rate limiting, account lockout, user enumeration prevention, data enumeration prevention across analysis and composite endpoints, NaN and empty-input guards across the processing pipeline, and a handful of race-condition and validation bugs.

Developer Journal

Auth rate limiting + lockout + enumeration prevention (#1187)

The big one. Three related changes in the auth flow:

Rate limiting/api/auth/login and /api/auth/register now have per-IP rate limits (5 attempts per minute, 20 per hour) via Microsoft.AspNetCore.RateLimiting. Returns 429 with retry-after when exceeded.

Account lockout — after 5 failed login attempts on a single account within 15 minutes, the account locks for 30 minutes. The lock only applies to that account, so one attacker can't lock everyone out by enumerating usernames.

Enumeration prevention — both login and registration now return identical response shapes and timings for "user exists" vs "user does not exist" vs "wrong password." Previously, login returned different response bodies for "user not found" vs "password wrong," which let an attacker enumerate valid usernames. Now it's always the same generic message with constant-time password checks against a dummy hash when the user doesn't exist.

The constant-time trick is important — if the response time differs by 100ms depending on whether the user exists, that's a side channel even if the response body is identical. Added tests that measure timing and assert it's within a tight tolerance for both cases.

Prevent data enumeration via analysis and composite endpoints (#1174)

Separate from the auth fix, the analysis and composite endpoints were returning different errors for "not found" vs "not shared with you." An attacker who shouldn't have access to target X could still learn that X exists by seeing "not shared" instead of "not found." Normalized to always return 404 regardless — the server knows which one is true, the client can't tell.

NaN and empty-input guards (#1176, #1181, #1183, #1177)

Four PRs hardening the processing pipeline inputs:

  • #1176normalize_array() was returning NaN when given all-NaN or empty input. Now guards up front, raises ValidationException with a clear message.
  • #1181create_region_mask() was building masks from coordinate pairs without bounds-checking. Added checks that RA/Dec are in range and the radius is positive.
  • #1183 — similarity search was returning NaN scores when one side of the comparison had no features. Now filters NaN scores out before ranking and returns empty results with a warning log rather than a corrupted ranking.
  • #1177downscale_for_composite() was accepting max_pixels without validating the target size would fit in memory. Added explicit validation that rejects unreasonable targets early.

Pagination and file handling fixes

  • #1180 — MAST service pagination was applying limit before offset, returning a smaller-than-requested page when offset was non-zero. Fixed order.
  • #1182resolve_s3_keys_from_products() was crashing on products missing required fields. Added explicit validation that rejects malformed products with a clear error rather than an opaque AttributeError.
  • #1178 — partial file mtime access was racing against the writer. The reader could hit the file between fopen and first write, get mtime 0, and misclassify as stale. Added retry with backoff.
  • #1179 — search results were missing the SharedWith data on the target DTO, so the UI couldn't render the "shared with" badge. Wired it through.

Cache eviction hardening (#1184, #1185)

  • #1184temp_cache eviction was firing but not verifying that eviction actually freed the target amount of space. On a shared filesystem where other processes could be holding file handles, eviction could succeed logically but fail physically. Now logs the before/after free space and warns if eviction didn't achieve the goal.
  • #1185cacheUtils on the frontend had a retry loop with no upper bound. If eviction kept failing (disk full and browsers refusing to evict), the tab would freeze. Capped at 3 retries with exponential backoff, then surface the error to the user.

Observe background tasks in MastController (#1188)

Background Tasks kicked off from controllers were being fire-and-forget — if one threw, the exception went to TaskScheduler.UnobservedTaskException and was silently logged at best. Wrapped all the fire-and-forget tasks in _ = SafeExecuteAsync(() => ...) which logs with the controller context and the job ID, and reports to the job tracker if the task was tied to a job. No more silently dropped background work.

Side note: Claude Desktop terminal integration

Claude Desktop got a significant overhaul — it can now run local sessions directly with terminal integration, which makes some workflows nicer than the CLI for interactive exploration. Skills work there but hooks don't, so it's a complementary tool rather than a replacement. A friend joked about how long until the AI decides rm -rf solves all debug issues; the honest answer is "trust but verify, and never bypass permissions mode."

What shipped

PR Title
#1188 fix: observe background tasks in MastController to prevent silent failures
#1187 feat: add auth rate limiting, account lockout, and user enumeration prevention
#1185 fix: bound eviction retry loop in cacheUtils to prevent tab freeze
#1184 fix: log and verify cache eviction results in temp_cache
#1183 fix: guard against NaN scores and empty results in similarity search
#1182 fix: validate required fields in resolve_s3_keys_from_products
#1181 fix: validate coordinate bounds in region mask creation
#1180 fix: apply limit after offset in MAST service pagination
#1179 fix: include SharedWith data in search results
#1178 fix: handle race condition when accessing partial file mtime
#1177 fix: validate max_pixels and array dimensions in downscale_for_composite
#1176 fix: guard normalize_array() against empty and all-NaN arrays
#1174 fix: prevent data enumeration via analysis and composite endpoints