fix(security): resolve all 17 codex findings — auth, grants, crypto, ops
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

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:
Alejandro Gutiérrez
2026-04-15 19:18:25 +01:00
parent 1a7a059e75
commit 2be5e9dccb
12 changed files with 464 additions and 341 deletions

View File

@@ -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 };
});
}

View File

@@ -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 {}
}