Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/snaps-controllers/src/snaps/SnapController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6087,6 +6087,94 @@ describe('SnapController', () => {
snapController.destroy();
});

it('supports preinstalled snaps with two-way initial connections', async () => {
const rootMessenger = getControllerMessenger();
jest.spyOn(rootMessenger, 'call');

rootMessenger.registerActionHandler(
'PermissionController:getPermissions',
(origin) => {
if (origin === `${MOCK_SNAP_ID}2`) {
return {
[WALLET_SNAP_PERMISSION_KEY]: {
caveats: [
{
type: SnapCaveatType.SnapIds,
value: {
[MOCK_SNAP_ID]: {},
},
},
],
date: 1664187844588,
id: 'izn0WGUO8cvq_jqvLQuQP',
invoker: MOCK_ORIGIN,
parentCapability: WALLET_SNAP_PERMISSION_KEY,
},
};
}

return {};
},
);

const preinstalledSnaps = [
{
snapId: MOCK_SNAP_ID,
manifest: getSnapManifest({
initialConnections: {
[`${MOCK_SNAP_ID}2`]: {},
},
}),
files: [
{
path: DEFAULT_SOURCE_PATH,
value: stringToBytes(DEFAULT_SNAP_BUNDLE),
},
{
path: DEFAULT_ICON_PATH,
value: stringToBytes(DEFAULT_SNAP_ICON),
},
],
},
{
snapId: `${MOCK_SNAP_ID}2` as SnapId,
manifest: getSnapManifest({
initialConnections: {
[MOCK_SNAP_ID]: {},
},
}),
files: [
{
path: DEFAULT_SOURCE_PATH,
value: stringToBytes(DEFAULT_SNAP_BUNDLE),
},
{
path: DEFAULT_ICON_PATH,
value: stringToBytes(DEFAULT_SNAP_ICON),
},
],
},
];

const snapControllerOptions = getSnapControllerWithEESOptions({
preinstalledSnaps,
rootMessenger,
});
const [snapController] = getSnapControllerWithEES(snapControllerOptions);

expect(snapControllerOptions.messenger.call).not.toHaveBeenCalledWith(
'PermissionController:revokePermissions',
{ [MOCK_SNAP_ID]: ['wallet_snap'] },
);

expect(snapControllerOptions.messenger.call).not.toHaveBeenCalledWith(
'PermissionController:revokePermissions',
{ [`${MOCK_SNAP_ID}2`]: ['wallet_snap'] },
);

snapController.destroy();
});

it('supports preinstalled snaps with initial connections', async () => {
const rootMessenger = getControllerMessenger();
jest.spyOn(rootMessenger, 'call');
Expand Down
6 changes: 5 additions & 1 deletion packages/snaps-controllers/src/snaps/SnapController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,7 @@ export class SnapController extends BaseController<
closeAllConnections,
messenger,
state,
dynamicPermissions = ['endowment:caip25'],
dynamicPermissions = ['endowment:caip25', 'wallet_snap'],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Broad Revocation of Wallet Permissions Globally Described

Adding 'wallet_snap' to the default dynamicPermissions array makes it revocable via revokeDynamicSnapPermissions for all snaps globally, not just those with initial connections. This could allow unintended revocation of wallet_snap permissions that were granted through other mechanisms, potentially breaking the permission model. The fix should be more targeted to only preserve initial connection permissions during updates, rather than making wallet_snap universally dynamic.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is known and intentional as per the PR description.

environmentEndowmentPermissions = [],
excludedPermissions = {},
idleTimeCheckInterval = inMilliseconds(5, Duration.Second),
Expand Down Expand Up @@ -4305,6 +4305,10 @@ export class SnapController extends BaseController<

// If the permission doesn't have dependencies, or if at least one of
// its dependencies is desired, include it in the desired permissions.
// NOTE: This effectively means that any permissions granted in the manifest
// that are considered dynamic, will not be automatically revoked
// when the permission is removed from the manifest.
// TODO: Deal with this technical debt.
if (!hasDependencies || hasDependency) {
accumulator[permissionName] = oldPermissions[permissionName];
}
Expand Down
Loading