fix: block sub-keys from accessing/modifying email, account key, and other sub-keys

This commit is contained in:
M1 2026-03-18 11:51:52 +04:00
parent eeb0318c4d
commit f7d6eff972
2 changed files with 11 additions and 5 deletions

View File

@ -141,7 +141,8 @@ export const account = new Elysia({ prefix: "/account" })
}; };
}) })
.post("/email", async ({ accountId, body }) => { .post("/email", async ({ accountId, keyId, body, error }) => {
if (keyId) return error(403, { error: "Sub-keys cannot modify account email" });
const emailHash = body.email ? hashEmail(body.email) : null; const emailHash = body.email ? hashEmail(body.email) : null;
await sql`UPDATE accounts SET email_hash = ${emailHash} WHERE id = ${accountId}`; await sql`UPDATE accounts SET email_hash = ${emailHash} WHERE id = ${accountId}`;
return { ok: true }; return { ok: true };
@ -151,14 +152,16 @@ export const account = new Elysia({ prefix: "/account" })
}), }),
}) })
.post("/reset-key", async ({ accountId, cookie }) => { .post("/reset-key", async ({ accountId, keyId, cookie, error }) => {
if (keyId) return error(403, { error: "Sub-keys cannot rotate the account key" });
const key = generateKey(); const key = generateKey();
await sql`UPDATE accounts SET key = ${key} WHERE id = ${accountId}`; await sql`UPDATE accounts SET key = ${key} WHERE id = ${accountId}`;
cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); cookie.pingql_key.set({ value: key, ...COOKIE_OPTS });
return { key, message: "Primary key rotated. Your old key is now invalid." }; return { key, message: "Primary key rotated. Your old key is now invalid." };
}) })
.post("/keys", async ({ accountId, body }) => { .post("/keys", async ({ accountId, keyId, body, error }) => {
if (keyId) return error(403, { error: "Sub-keys cannot create other sub-keys" });
const key = generateKey(); const key = generateKey();
const [created] = await sql`INSERT INTO api_keys (key, account_id, label) VALUES (${key}, ${accountId}, ${body.label}) RETURNING id`; const [created] = await sql`INSERT INTO api_keys (key, account_id, label) VALUES (${key}, ${accountId}, ${body.label}) RETURNING id`;
return { key, id: created.id, label: body.label }; return { key, id: created.id, label: body.label };
@ -168,7 +171,8 @@ export const account = new Elysia({ prefix: "/account" })
}), }),
}) })
.delete("/keys/:id", async ({ accountId, params, error }) => { .delete("/keys/:id", async ({ accountId, keyId, params, error }) => {
if (keyId) return error(403, { error: "Sub-keys cannot revoke other sub-keys" });
const [deleted] = await sql` const [deleted] = await sql`
DELETE FROM api_keys WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id DELETE FROM api_keys WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id
`; `;

View File

@ -31,7 +31,8 @@
</div> </div>
</section> </section>
<!-- Email --> <!-- Email (hidden for sub-key sessions) -->
<% if (!it.isSubKey) { %>
<section class="bg-gray-900 rounded-xl border border-gray-800 p-6"> <section class="bg-gray-900 rounded-xl border border-gray-800 p-6">
<h2 class="text-sm font-semibold text-gray-300 mb-1">Recovery Email</h2> <h2 class="text-sm font-semibold text-gray-300 mb-1">Recovery Email</h2>
<p class="text-xs text-gray-600 mb-4">Used for account recovery only. Stored as a one-way hash — we can't read it.</p> <p class="text-xs text-gray-600 mb-4">Used for account recovery only. Stored as a one-way hash — we can't read it.</p>
@ -44,6 +45,7 @@
</div> </div>
<p id="email-status" class="text-xs mt-2 hidden"></p> <p id="email-status" class="text-xs mt-2 hidden"></p>
</section> </section>
<% } %>