Skip to content

Commit 0c8a77e

Browse files
Add permission check
1 parent 9a13069 commit 0c8a77e

File tree

2 files changed

+30
-4
lines changed

2 files changed

+30
-4
lines changed

packages/snaps-controllers/src/interface/SnapInterfaceController.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
validateInterfaceContext,
4242
} from './utils';
4343
import type { GetSnap } from '../snaps';
44+
import { HasPermission } from '@metamask/permission-controller';
4445

4546
const MAX_UI_CONTENT_SIZE = 10_000_000; // 10 mb
4647

@@ -114,7 +115,8 @@ export type SnapInterfaceControllerAllowedActions =
114115
| MultichainAssetsControllerGetStateAction
115116
| AccountsControllerGetSelectedMultichainAccountAction
116117
| AccountsControllerGetAccountByAddressAction
117-
| AccountsControllerListMultichainAccountsAction;
118+
| AccountsControllerListMultichainAccountsAction
119+
| HasPermission;
118120

119121
export type SnapInterfaceControllerActions =
120122
| CreateInterface
@@ -282,7 +284,7 @@ export class SnapInterfaceController extends BaseController<
282284
contentType?: ContentType,
283285
) {
284286
const element = getJsxInterface(content);
285-
this.#validateContent(element);
287+
this.#validateContent(snapId, element);
286288
validateInterfaceContext(context);
287289

288290
const id = nanoid();
@@ -339,7 +341,7 @@ export class SnapInterfaceController extends BaseController<
339341
) {
340342
this.#validateArgs(snapId, id);
341343
const element = getJsxInterface(content);
342-
this.#validateContent(element);
344+
this.#validateContent(snapId, element);
343345
validateInterfaceContext(context);
344346

345347
const oldState = this.state.interfaces[id].state;
@@ -530,13 +532,22 @@ export class SnapInterfaceController extends BaseController<
530532
return this.messenger.call('SnapController:get', id);
531533
}
532534

535+
#hasPermission(snapId: SnapId, permission: string) {
536+
return this.messenger.call(
537+
'PermissionController:hasPermission',
538+
snapId,
539+
permission,
540+
);
541+
}
542+
533543
/**
534544
* Utility function to validate the components of an interface.
535545
* Throws if something is invalid.
536546
*
547+
* @param snapId - The Snap ID.
537548
* @param element - The JSX element to verify.
538549
*/
539-
#validateContent(element: JSXElement) {
550+
#validateContent(snapId: SnapId, element: JSXElement) {
540551
// We assume the validity of this JSON to be validated by the caller.
541552
// E.g., in the RPC method implementation.
542553
const size = getJsonSizeUnsafe(element);
@@ -549,6 +560,7 @@ export class SnapInterfaceController extends BaseController<
549560
isOnPhishingList: this.#checkPhishingList.bind(this),
550561
getSnap: this.#getSnap.bind(this),
551562
getAccountByAddress: this.#getAccountByAddress.bind(this),
563+
hasPermission: this.#hasPermission.bind(this, snapId),
552564
});
553565
}
554566

packages/snaps-utils/src/ui.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NodeType } from '@metamask/snaps-sdk';
33
import type {
44
BoldChildren,
55
GenericSnapElement,
6+
ImageElement,
67
ItalicChildren,
78
JSXElement,
89
LinkElement,
@@ -42,6 +43,7 @@ import type { Token, Tokens } from 'marked';
4243
import type { InternalAccount } from './account';
4344
import type { Snap } from './snaps';
4445
import { parseMetaMaskUrl } from './url';
46+
import { isValidUrl } from './types';
4547

4648
const MAX_TEXT_LENGTH = 50_000; // 50 kb
4749
const ALLOWED_PROTOCOLS = ['https:', 'mailto:', 'metamask:'];
@@ -433,19 +435,22 @@ export function validateAssetSelector(
433435
* phishing list.
434436
* @param hooks.getSnap - The function that returns a snap if installed, undefined otherwise.
435437
* @param hooks.getAccountByAddress - The function that returns an account by address.
438+
* @param hooks.hasPermission - A function that checks whether the Snap has a given permission.
436439
*/
437440
export function validateJsxElements(
438441
node: JSXElement,
439442
{
440443
isOnPhishingList,
441444
getSnap,
442445
getAccountByAddress,
446+
hasPermission,
443447
}: {
444448
isOnPhishingList: (url: string) => boolean;
445449
getSnap: (id: string) => Snap | undefined;
446450
getAccountByAddress: (
447451
address: CaipAccountId,
448452
) => InternalAccount | undefined;
453+
hasPermission: (permission: string) => boolean;
449454
},
450455
) {
451456
walkJsx(node, (childNode) => {
@@ -461,6 +466,15 @@ export function validateJsxElements(
461466
getAccountByAddress,
462467
);
463468
break;
469+
case 'Image': {
470+
const { src } = (childNode as ImageElement).props;
471+
const isUrl = isValidUrl(src);
472+
assert(
473+
!isUrl || (isUrl && hasPermission('endowment:network-access')),
474+
'Using external images is only permitted with the network access endowment.',
475+
);
476+
break;
477+
}
464478
default:
465479
break;
466480
}

0 commit comments

Comments
 (0)