fix(security): resolve all 17 codex findings — auth, grants, crypto, ops
Critical: broker HTTP auth via cli_session bearer token on all /cli/*; file download requires auth+membership; v2 claim gated; duplicate claimInviteV2Core removed; grant enforcement tries member then session pubkey; audit hash uses canonical sorted-keys JSON. High: rate limit args fixed (burst 10, 60/min) + both buckets swept; BROKER_ENCRYPTION_KEY fail-fast in prod; migrate uses pg_try + lock_ timeout; hello validates sessionPubkey hex; blocked DMs rejected pre- queue; watch timers cleaned on disconnect. Medium: inbound pushes serialized; reconnect jitter + timer guard; hardcoded URLs through env; v2 claim path configurable. Low: WSHelloMessage optional protocolVersion+capabilities. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,18 +35,13 @@ const BROKER_HTTP = URLS.BROKER.replace("wss://", "https://").replace("ws://", "
|
||||
async function syncToBroker(meshSlug: string, grants: Record<string, string[] | null>): Promise<void> {
|
||||
const auth = getStoredToken();
|
||||
if (!auth) return;
|
||||
let userId = "";
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(auth.session_token.split(".")[1]!, "base64url").toString()) as { sub?: string };
|
||||
userId = payload.sub ?? "";
|
||||
} catch { return; }
|
||||
if (!userId) return;
|
||||
try {
|
||||
await request<{ ok: true }>({
|
||||
path: `/cli/mesh/${meshSlug}/grants`,
|
||||
method: "POST",
|
||||
body: { user_id: userId, grants },
|
||||
body: { grants },
|
||||
baseUrl: BROKER_HTTP,
|
||||
token: auth.session_token,
|
||||
});
|
||||
} catch (e) {
|
||||
render.warn(`broker grant sync failed — client filter still active: ${e instanceof Error ? e.message : e}`);
|
||||
@@ -91,8 +86,20 @@ function resolveCaps(input: string[]): Capability[] {
|
||||
async function resolvePeer(meshSlug: string, name: string): Promise<{ displayName: string; pubkey: string } | null> {
|
||||
return await withMesh({ meshSlug }, async (client) => {
|
||||
const peers = await client.listPeers();
|
||||
const match = peers.find((p) => p.displayName === name || p.pubkey === name || p.pubkey.startsWith(name));
|
||||
return match ? { displayName: match.displayName, pubkey: match.pubkey } : null;
|
||||
const match = peers.find(
|
||||
(p) =>
|
||||
p.displayName === name ||
|
||||
p.pubkey === name ||
|
||||
p.pubkey.startsWith(name) ||
|
||||
p.memberPubkey === name ||
|
||||
(p.memberPubkey && p.memberPubkey.startsWith(name)),
|
||||
);
|
||||
if (!match) return null;
|
||||
// Prefer the stable member pubkey for grant keys — session pubkey
|
||||
// rotates on every reconnect and would invalidate the grant entry.
|
||||
// Broker falls back to session-key lookup for pre-alpha.36 clients.
|
||||
const key = match.memberPubkey ?? match.pubkey;
|
||||
return { displayName: match.displayName, pubkey: key };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,23 +25,16 @@ export async function runList(): Promise<void> {
|
||||
const config = readConfig();
|
||||
const auth = getStoredToken();
|
||||
|
||||
// Try to fetch from server
|
||||
// Try to fetch from server. Broker authenticates via Bearer token.
|
||||
let serverMeshes: ServerMesh[] = [];
|
||||
if (auth) {
|
||||
try {
|
||||
let userId = "";
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(auth.session_token.split(".")[1]!, "base64url").toString()) as { sub?: string };
|
||||
userId = payload.sub ?? "";
|
||||
} catch {}
|
||||
|
||||
if (userId) {
|
||||
const res = await request<{ meshes: ServerMesh[] }>({
|
||||
path: `/cli/meshes?user_id=${userId}`,
|
||||
baseUrl: BROKER_HTTP,
|
||||
});
|
||||
serverMeshes = res.meshes ?? [];
|
||||
}
|
||||
const res = await request<{ meshes: ServerMesh[] }>({
|
||||
path: `/cli/meshes`,
|
||||
baseUrl: BROKER_HTTP,
|
||||
token: auth.session_token,
|
||||
});
|
||||
serverMeshes = res.meshes ?? [];
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user