mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
A device row carries two fields that are not ordinary data: device_token, the credential the player proves with on the /device socket, and settings_pin, which unlocks the player's on-device settings menu and so hands physical control of the panel to anyone holding it. device_token was already stripped everywhere. settings_pin was not — it went out on both the collection and the detail endpoint. The dashboard does show it, but on one screen only: the device detail page, which fetches a single device. The collection endpoint had no consumer for it and was returning the PIN for every device in the workspace on every load. The detail endpoint keeps it, so that page is unchanged. The list no longer sends it. Same data, much smaller blast radius, no feature lost. Tests pin the split in both directions — absent from the list, present on the detail, and the socket credential absent from both (asserted on the whole serialized payload, not just the top-level key, so a nested echo would fail too). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
31 lines
1.3 KiB
JavaScript
31 lines
1.3 KiB
JavaScript
'use strict';
|
|
|
|
// Security: never return a device's WebSocket auth secret to API/dashboard
|
|
// clients. `device_token` is the credential the device proves with (validated
|
|
// via crypto.timingSafeEqual on the /device socket); leaking it to any
|
|
// workspace user enables device impersonation. Strip it from every device row
|
|
// before it leaves the server.
|
|
function stripDeviceSecrets(d) {
|
|
if (!d || typeof d !== 'object') return d;
|
|
delete d.device_token;
|
|
return d;
|
|
}
|
|
|
|
// List responses additionally drop `settings_pin`.
|
|
//
|
|
// The PIN unlocks the player's on-device settings menu (2x Back), i.e. physical control of
|
|
// the panel. The dashboard genuinely needs it — but only on ONE screen, the device detail
|
|
// page, which fetches a single device via GET /api/devices/:id. The collection endpoint was
|
|
// handing out the PIN for EVERY device in the workspace on every load, to every member,
|
|
// with no consumer for it. Same data, far wider blast radius, for nothing.
|
|
//
|
|
// So: detail keeps it (the feature is unchanged), the list does not. If a future list view
|
|
// needs the PIN, fetch the device rather than widening this.
|
|
function stripDeviceSecretsForList(d) {
|
|
const row = stripDeviceSecrets(d);
|
|
if (row && typeof row === 'object') delete row.settings_pin;
|
|
return row;
|
|
}
|
|
|
|
module.exports = { stripDeviceSecrets, stripDeviceSecretsForList };
|