mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 08:15:55 +00:00
fix: keep blob uploads privileged-only and derive the WebAuthn salt from the calling plugin
This commit is contained in:
@@ -305,3 +305,25 @@ describe('native keyword extension API', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('privileged-only methods', () => {
|
||||
it('refuses jmap.uploadBlob to an untrusted plugin even with email:blob-write', async () => {
|
||||
const p = plugin({ permissions: ['email:blob-write'], grantedPermissions: ['email:blob-write'] });
|
||||
expect(await gateError(p, 'jmap.uploadBlob', [new Uint8Array(1), 'a.bin', 'application/octet-stream']))
|
||||
.toMatch(/requires the privileged plugin tier/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebAuthn PRF salt', () => {
|
||||
it("uses the calling plugin's id, not the one in the arguments", async () => {
|
||||
const get = vi.fn(async () => { throw new Error('stop'); });
|
||||
Object.defineProperty(navigator, 'credentials', { value: { get }, configurable: true });
|
||||
const p = plugin({ id: 'mine', permissions: ['crypto:full'], managed: true, tier: 'privileged' });
|
||||
|
||||
await dispatchApiCall(p, 'crypto.getWebAuthn', [[1, 2, 3], 'someone-else'], { privileged: true }).catch(() => {});
|
||||
|
||||
const salt = (get.mock.calls[0] as unknown as [{ publicKey: { extensions: { prf: { eval: { first: Uint8Array } } } } }])[0]
|
||||
.publicKey.extensions.prf.eval.first;
|
||||
expect(new TextDecoder().decode(salt)).toBe('bulwark-plugins-v1mine');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,8 @@ import { createHash } from 'crypto';
|
||||
*/
|
||||
const PRIVILEGED_ONLY_METHODS = new Set<string>([
|
||||
'jmap.fetchBlob',
|
||||
// Puts arbitrary bytes on the mail server. Untrusted plugins attach files
|
||||
// through the composer and draft APIs instead, which bound and label them.
|
||||
'jmap.uploadBlob',
|
||||
'jmap.sendRaw',
|
||||
'jmap.submitRaw',
|
||||
@@ -1556,8 +1558,10 @@ export async function dispatchApiCall(
|
||||
case 'upfiles.get' : return getFile(args[0] as string);
|
||||
case 'upfiles.save' : return saveFile(args[0] as string, args[1] as File);
|
||||
|
||||
case 'crypto.createWebAuthn': return doCreatePRF(args[0] as string, args[1] as string, args[2] as string);
|
||||
case 'crypto.getWebAuthn': return doGetPRF(args[0] as number[], args[1] as string);
|
||||
// The PRF salt is derived from the calling plugin's id, never from an
|
||||
// argument: otherwise one plugin could ask for another plugin's secret.
|
||||
case 'crypto.createWebAuthn': return doCreatePRF(plugin.id, args[1] as string, args[2] as string);
|
||||
case 'crypto.getWebAuthn': return doGetPRF(args[0] as number[], plugin.id);
|
||||
case 'crypto.getPublicKeys': return getPublicKeys();
|
||||
case 'crypto.createPublicKey': return doCreatePublicKey(args[0] as PublicKeyInput);
|
||||
case 'crypto.removePublicKey': return doRemovePublicKey(args[0] as string);
|
||||
|
||||
Reference in New Issue
Block a user