fix(door): never return raw controller reason to the client (D10)
Every door action bubbled the executor's reason/e.message to the client; redaction was password-literal + a fixed param-name regex, so a generic-adapter connection with a custom auth param + verbose upstream error could surface the secret client-side. Client returns are now a generic 'controller call failed' message; the redacted detail still goes to door_access_log for operators. redactDoorSecrets + callDoor gained an optional authParam (scrub the connection's actual auth param value regardless of name); threading conn.auth_param through the door-call selects is a deferred defense-in-depth follow-up (client leak is fully closed; native HeatSync e= is already regex-covered).
This commit is contained in:
+5
-5
@@ -282,7 +282,7 @@ export async function testDoorConnection(input: unknown) {
|
||||
revalidatePath('/door/manage')
|
||||
return result.ok
|
||||
? { data: { ok: true, status: result.status } }
|
||||
: { error: result.reason }
|
||||
: { error: 'The door controller call failed. Check the door audit log for the redacted detail.' }
|
||||
}
|
||||
|
||||
export async function listDoorAccessLog() {
|
||||
@@ -511,7 +511,7 @@ export async function grantCard(input: unknown) {
|
||||
targetMemberId, action: 'grant', success: false,
|
||||
detail: `failed: ${result.reason}`, password,
|
||||
})
|
||||
return { error: result.reason }
|
||||
return { error: 'The door controller call failed. Check the door audit log for the redacted detail.' }
|
||||
}
|
||||
|
||||
await auditDoor(admin, {
|
||||
@@ -590,7 +590,7 @@ export async function revokeCard(input: unknown) {
|
||||
targetMemberId, action: 'revoke', success: false,
|
||||
detail: `failed (slot ${slot} kept): ${result.reason}`, password,
|
||||
})
|
||||
return { error: result.reason }
|
||||
return { error: 'The door controller call failed. Check the door audit log for the redacted detail.' }
|
||||
}
|
||||
|
||||
await admin.from('door_card_slots').delete().eq('connection_id', connectionId).eq('card_id', cardId)
|
||||
@@ -688,7 +688,7 @@ export async function doorControl(input: unknown) {
|
||||
})
|
||||
|
||||
revalidatePath('/door/manage')
|
||||
return result.ok ? { data: { ok: true, status: result.status } } : { error: result.reason }
|
||||
return result.ok ? { data: { ok: true, status: result.status } } : { error: 'The door controller call failed. Check the door audit log for the redacted detail.' }
|
||||
}
|
||||
|
||||
// ─── Member self-entry ("buzz me in") ────────────────────────────────────────
|
||||
@@ -799,7 +799,7 @@ export async function selfEntry(input: unknown) {
|
||||
})
|
||||
|
||||
revalidatePath('/dashboard')
|
||||
return result.ok ? { data: { ok: true } } : { error: result.reason }
|
||||
return result.ok ? { data: { ok: true } } : { error: 'The door controller call failed. Check the door audit log for the redacted detail.' }
|
||||
}
|
||||
|
||||
// The signed-in member's OWN door activity (rows where they were the actor or
|
||||
|
||||
+15
-1
@@ -211,11 +211,25 @@ export function applyTemplate(
|
||||
// the shared door password: any literal occurrence, and any e=/&e= query
|
||||
// value (the firmware auth param), become "<redacted>". The password is a
|
||||
// credential and must never land in door_access_log.
|
||||
export function redactDoorSecrets(text: string, password?: string | null): string {
|
||||
export function redactDoorSecrets(
|
||||
text: string,
|
||||
password?: string | null,
|
||||
authParam?: string | null,
|
||||
): string {
|
||||
let out = text
|
||||
if (password && password.length > 0) {
|
||||
out = out.split(password).join('<redacted>')
|
||||
}
|
||||
// The connection's actual auth query param (generic adapters may name it
|
||||
// anything, not just e=/pw=). Scrub its value regardless of the name so a
|
||||
// verbose upstream error echoing the URL cannot leak the secret even when
|
||||
// `password` is empty/unknown.
|
||||
if (authParam && /^\w+$/.test(authParam)) {
|
||||
out = out.replace(
|
||||
new RegExp(`([?&]${authParam}=)[^&\\s"']*`, 'gi'),
|
||||
'$1<redacted>',
|
||||
)
|
||||
}
|
||||
out = out.replace(/([?&](?:e|pw|password|api[_-]?key|token)=)[^&\s"']*/gi, '$1<redacted>')
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export async function callDoor(opts: {
|
||||
url: string
|
||||
pinnedHost: string
|
||||
password?: string | null
|
||||
authParam?: string | null
|
||||
timeoutMs?: number
|
||||
}): Promise<DoorCallResult> {
|
||||
const guard = validateDoorTarget(opts.url, opts.pinnedHost)
|
||||
@@ -57,11 +58,11 @@ export async function callDoor(opts: {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
const snippet = redactDoorSecrets(received.slice(0, SNIPPET), opts.password)
|
||||
const snippet = redactDoorSecrets(received.slice(0, SNIPPET), opts.password, opts.authParam)
|
||||
return { ok: res.ok, status: res.status, snippet }
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'request failed'
|
||||
return { ok: false, reason: redactDoorSecrets(msg, opts.password) }
|
||||
return { ok: false, reason: redactDoorSecrets(msg, opts.password, opts.authParam) }
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user