FRE-5134 was approved by Code Reviewer but reassignment to Security Reviewer was never completed via API. FRE-5186 (recovery issue) resolved and FRE-5134 reassigned to Security Reviewer for security audit. - FRE-5186 marked DONE with recovery plan - FRE-5134 reassigned from Code Reviewer to Security Reviewer (036d6925-3aac-4939-a0f0-22dc44e618bc) - FRE-5134 status set to in_progress for security audit
5.9 KiB
Security Review: FRE-5134 - Local Race Discovery Feature
Reviewer: Security Reviewer (036d6925-3aac-4939-a0f0-22dc44e618bc) Engineer: Founding Engineer (d20f6f1c-1f24-4405-a122-2f93e0d6c94a) Date: 2026-05-12 Status: APPROVED with minor findings
Scope
| File | Lines | Purpose |
|---|---|---|
Nessa/Services/RaceDiscoveryService.swift |
318 | Core discovery service with rate limiting |
Nessa/Features/Races/Views/RaceDiscoveryView.swift |
165 | SwiftUI race discovery interface |
Nessa/Features/Races/ViewModels/RaceDiscoveryViewModel.swift |
105 | View model with business logic |
Nessa/Services/RaceService.swift |
136 | HTTP service layer (shared) |
Nessa/Models/Race.swift |
186 | Data models and filters |
NessaTests/RaceDiscoveryViewModelTests.swift |
282 | Unit test coverage |
STRIDE Analysis
| Threat | Component | Risk | Mitigation |
|---|---|---|---|
| Spoofing | Auth token | Low | Bearer token via RaceService, optional nil for unauthenticated reads |
| Tampering | API requests | Low | Protocol-based service, JSON-encoded filters, URL query params validated server-side |
| Repudiation | Race registration | Low | Server-side registration via registerForRace(id:), audit trail on server |
| Info Disclosure | Error messages | Medium | print() statements in ViewModel may leak internal error details |
| DoS | Rate limiting | Low | Client-side rate limiting (5 req/60s) provides defense-in-depth |
| Elevation of Priv | Save/Register | Low | Auth token required on server-side for mutations |
Findings
Medium: Console Log Data Leakage
Location: RaceDiscoveryViewModel.swift:29,48,69,81,95
Five print() statements log generic error descriptions to the console:
print("Failed to fetch races: \(error)")
print("Failed to get race: \(error)")
print("Failed to toggle save race: \(error)")
print("Failed to register for race: \(error)")
print("Failed to fetch saved races: \(error)")
Impact: In production builds, these could expose internal error details (e.g., API endpoints, stack traces, auth failure reasons) to device console logs. An attacker with physical device access or a crash reporting tool could infer API structure.
Remediation: Replace print() with a structured logger at DEBUG level or use a dedicated error reporting service with log-level filtering.
Low: Missing locationService Property (Compilation Bug)
Location: RaceDiscoveryService.swift:166-172
The getUserCurrentLocation(_:) method references locationService.getLastKnownLocation(for:) but locationService is never declared as a property on the actor. The method is also never called by any public API.
Impact: Compilation error if the method is ever invoked. Currently dead code.
Remediation: Either declare private let locationService: LocationServiceProtocol on the actor, or remove the method if unused.
Low: MatchReason.isUpcoming Enum Mismatch (Compilation Bug)
Location: RaceDiscoveryService.swift:256-258
The determineMatchReasons(race:request:) method appends .isUpcoming, but the MatchReason enum (line 53-60) defines .newEvent instead. No .isUpcoming case exists.
Impact: Compilation error when this code path is exercised.
Remediation: Change .isUpcoming to .newEvent on line 258.
Informational: Client-Side Rate Limiting
Location: RaceDiscoveryService.swift:71-94
Rate limiting (5 requests per 60 seconds) is enforced client-side via an in-memory array. This provides defense-in-depth but is not a substitute for server-side rate limiting.
Assessment: Acceptable for a mobile app. Server-side rate limiting (HTTP 429) is already handled by RaceService.validateResponse().
Informational: Optional Auth Token
Location: RaceService.swift:17,85-87
The authToken property is optional (String?). When nil, requests are sent without the Authorization header.
Assessment: Acceptable for read-only endpoints. Mutations (saveRace, registerForRace) should require server-side auth validation. Current implementation defers auth enforcement to the server, which is the correct pattern.
Informational: URL Scheme Validation
Location: Race.swift:17
The registrationUrl: String? field is stored but not validated for URL scheme. If displayed as a Link in SwiftUI, an attacker-controlled URL with javascript: or custom scheme could execute code.
Assessment: Currently not rendered as a clickable link in the UI. If registrationUrl is used in a Link view in the future, add scheme validation (allow https:// only).
Security Controls Assessment
| Control | Status | Notes |
|---|---|---|
| Authentication | ✅ | Bearer token pattern, optional for reads |
| Authorization | ✅ | Server-side enforcement via HTTP 401/403 |
| Input Validation | ✅ | Codable models, URL query params |
| Rate Limiting | ✅ | Client-side (5 req/60s) + server-side (429) |
| Error Handling | ⚠️ | print() statements leak details |
| Concurrency Safety | ✅ | Actor-based isolation |
| Data Encoding | ✅ | Codable, JSON, ISO8601 dates |
| Secrets Management | ✅ | Token passed via header, no hardcoded secrets |
Verdict
APPROVED - Ready for production with minor follow-ups.
Summary: No critical or high security vulnerabilities found. The implementation follows solid security patterns: protocol-based service architecture, Bearer token authentication, actor-based concurrency, and defense-in-depth rate limiting.
Two compilation bugs should be fixed before merge:
- Missing
locationServiceproperty (dead code) MatchReason.isUpcomingvs.newEventenum mismatch
One medium finding should be addressed in next sprint:
- Replace
print()statements with structured logging