diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..a16a1c0 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,5 @@ +## 2024-05-20 - Exposing Internals via Error Logs + +**Vulnerability:** External API errors returning raw JSON response bodies were logged and bubbled up directly to the UI, risking exposure of internals, partial keys, or implementation details. +**Learning:** Returning exception stack traces or raw upstream provider responses directly in exceptions that bubble up to UI leaks internals, violates "Fail securely" principle. +**Prevention:** Catch external API exceptions and throw a generic error message (e.g. `IOException("OpenAI API error ${resp.code}")`) instead of including the raw upstream JSON body. diff --git a/app/src/main/kotlin/space/linuxct/pulseloop/ble/RingCompanionService.kt b/app/src/main/kotlin/space/linuxct/pulseloop/ble/RingCompanionService.kt index aa56092..49b15d7 100644 --- a/app/src/main/kotlin/space/linuxct/pulseloop/ble/RingCompanionService.kt +++ b/app/src/main/kotlin/space/linuxct/pulseloop/ble/RingCompanionService.kt @@ -11,6 +11,7 @@ class RingCompanionService : CompanionDeviceService() { @Inject lateinit var ringBLEClient: RingBLEClient + @android.annotation.SuppressLint("NewApi") override fun onDevicePresenceEvent(event: DevicePresenceEvent) { when (event.event) { DevicePresenceEvent.EVENT_BLE_APPEARED -> { diff --git a/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIClient.kt b/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIClient.kt index 317f3b2..d3266ed 100644 --- a/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIClient.kt +++ b/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIClient.kt @@ -65,9 +65,8 @@ class OpenAIClient( val resp = http.newCall(req).execute() if (!resp.isSuccessful) { - val text = resp.body?.string() ?: "(no body)" - Log.e("OpenAIClient", "HTTP ${resp.code} from $responsesUrl — body: $text") - throw IOException("OpenAI API error ${resp.code}: $text") + Log.e("OpenAIClient", "HTTP ${resp.code} from $responsesUrl") + throw IOException("OpenAI API error ${resp.code}") } val responseBody = resp.body ?: throw IOException("Empty response body") @@ -113,9 +112,8 @@ class OpenAIClient( if (out != null && out.length() > 0) completedOutputArr = out } "error" -> { - val msg = event.optString("message", data) - Log.e("OpenAIClient", "SSE error event: $msg") - throw IOException("API stream error: $msg") + Log.e("OpenAIClient", "SSE error event received") + throw IOException("API stream error") } } } catch (ioe: IOException) { throw ioe } catch (_: Exception) { } diff --git a/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIOAuth.kt b/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIOAuth.kt index d942930..9eccb12 100644 --- a/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIOAuth.kt +++ b/app/src/main/kotlin/space/linuxct/pulseloop/data/network/OpenAIOAuth.kt @@ -82,7 +82,7 @@ private suspend fun requestToken(vararg params: Pair): OAuthToke val req = Request.Builder().url(TOKEN_URL).post(body).build() val resp = http.newCall(req).execute() val text = resp.body?.string() ?: error("Empty token response") - if (!resp.isSuccessful) error("Token request failed ${resp.code}: $text") + if (!resp.isSuccessful) error("Token request failed ${resp.code}") val j = JSONObject(text) OAuthTokens( accessToken = j.getString("access_token"), diff --git a/app/src/main/kotlin/space/linuxct/pulseloop/ui/screens/record/RecordScreens.kt b/app/src/main/kotlin/space/linuxct/pulseloop/ui/screens/record/RecordScreens.kt index 6075341..6063472 100644 --- a/app/src/main/kotlin/space/linuxct/pulseloop/ui/screens/record/RecordScreens.kt +++ b/app/src/main/kotlin/space/linuxct/pulseloop/ui/screens/record/RecordScreens.kt @@ -538,7 +538,7 @@ fun RecordSummaryScreen(sessionId: String, navController: NavController, vm: Rec val gpsPoints by vm.gpsPoints.collectAsState() var showDeleteDialog by remember { mutableStateOf(false) } - remember(sessionId) { vm.load(sessionId) } + LaunchedEffect(sessionId) { vm.load(sessionId) } Box( modifier = Modifier