# 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: ```swift 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: 1. Missing `locationService` property (dead code) 2. `MatchReason.isUpcoming` vs `.newEvent` enum mismatch **One medium finding** should be addressed in next sprint: - Replace `print()` statements with structured logging