diff --git a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts index 854a36f450..fae47ba9e7 100644 --- a/packages/snaps-controllers/src/interface/SnapInterfaceController.ts +++ b/packages/snaps-controllers/src/interface/SnapInterfaceController.ts @@ -272,6 +272,7 @@ export class SnapInterfaceController extends BaseController< ); await this.#triggerPhishingListUpdate(); + // TODO: Expose configuration for allowedProtocols in validateJsxLinks validateJsxLinks(element, this.#checkPhishingList.bind(this)); } } diff --git a/packages/snaps-rpc-methods/src/restricted/notify.ts b/packages/snaps-rpc-methods/src/restricted/notify.ts index fdf3001be3..1155df448d 100644 --- a/packages/snaps-rpc-methods/src/restricted/notify.ts +++ b/packages/snaps-rpc-methods/src/restricted/notify.ts @@ -111,15 +111,19 @@ export const notifyBuilder = Object.freeze({ * @param hooks.showInAppNotification - A function that shows a notification in the MetaMask UI. * @param hooks.isOnPhishingList - A function that checks for links against the phishing list. * @param hooks.maybeUpdatePhishingList - A function that updates the phishing list if needed. + * @param allowedProtocols - Allowed protocols for links (example: ['https:']). * @returns The method implementation which returns `null` on success. * @throws If the params are invalid. */ -export function getImplementation({ - showNativeNotification, - showInAppNotification, - isOnPhishingList, - maybeUpdatePhishingList, -}: NotifyMethodHooks) { +export function getImplementation( + { + showNativeNotification, + showInAppNotification, + isOnPhishingList, + maybeUpdatePhishingList, + }: NotifyMethodHooks, + allowedProtocols?: string[], +) { return async function implementation( args: RestrictedMethodOptions, ): Promise { @@ -132,7 +136,11 @@ export function getImplementation({ await maybeUpdatePhishingList(); - validateTextLinks(validatedParams.message, isOnPhishingList); + validateTextLinks( + validatedParams.message, + isOnPhishingList, + allowedProtocols, + ); switch (validatedParams.type) { case NotificationType.Native: diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index ce1f9b87d8..6f02f472e8 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -1,5 +1,5 @@ { - "branches": 96.7, + "branches": 96.72, "functions": 98.72, "lines": 98.81, "statements": 94.79 diff --git a/packages/snaps-utils/src/ui.test.tsx b/packages/snaps-utils/src/ui.test.tsx index 1f61c52a0c..4768a2ab3a 100644 --- a/packages/snaps-utils/src/ui.test.tsx +++ b/packages/snaps-utils/src/ui.test.tsx @@ -615,6 +615,31 @@ describe('validateTextLinks', () => { 'Invalid URL: Unable to parse URL.', ); }); + + it('handles custom protocols', () => { + const allowedProtocols = ['http:', 'file:']; + expect(() => + validateTextLinks( + '[test](http://foo.bar)', + () => false, + allowedProtocols, + ), + ).not.toThrow(); + expect(() => + validateTextLinks( + '[test](https://foo.bar)', + () => false, + allowedProtocols, + ), + ).toThrow('Invalid URL: Protocol must be one of: http:, file:.'); + expect(() => + validateTextLinks( + '', + () => false, + allowedProtocols, + ), + ).not.toThrow(); + }); }); describe('validateJsxLinks', () => { diff --git a/packages/snaps-utils/src/ui.tsx b/packages/snaps-utils/src/ui.tsx index cad94f4d53..049d69d267 100644 --- a/packages/snaps-utils/src/ui.tsx +++ b/packages/snaps-utils/src/ui.tsx @@ -40,7 +40,7 @@ import { lexer, walkTokens } from 'marked'; import type { Token, Tokens } from 'marked'; const MAX_TEXT_LENGTH = 50_000; // 50 kb -const ALLOWED_PROTOCOLS = ['https:', 'mailto:']; +const DEFAULT_ALLOWED_PROTOCOLS = ['https:', 'mailto:']; /** * Get the button variant from a legacy button component variant. @@ -320,16 +320,18 @@ function getMarkdownLinks(text: string) { * @param link - The link to validate. * @param isOnPhishingList - The function that checks the link against the * phishing list. + * @param allowedProtocols - Allowed protocols (example: ['https:']). */ function validateLink( link: string, isOnPhishingList: (url: string) => boolean, + allowedProtocols: string[], ) { try { const url = new URL(link); assert( - ALLOWED_PROTOCOLS.includes(url.protocol), - `Protocol must be one of: ${ALLOWED_PROTOCOLS.join(', ')}.`, + allowedProtocols.includes(url.protocol), + `Protocol must be one of: ${allowedProtocols.join(', ')}.`, ); const hostname = @@ -352,16 +354,18 @@ function validateLink( * @param text - The text to verify. * @param isOnPhishingList - The function that checks the link against the * phishing list. + * @param allowedProtocols - Allowed protocols (example: ['https:']). * @throws If the text contains a link that is not allowed. */ export function validateTextLinks( text: string, isOnPhishingList: (url: string) => boolean, + allowedProtocols: string[] = DEFAULT_ALLOWED_PROTOCOLS, ) { const links = getMarkdownLinks(text); for (const link of links) { - validateLink(link.href, isOnPhishingList); + validateLink(link.href, isOnPhishingList, allowedProtocols); } } @@ -372,17 +376,19 @@ export function validateTextLinks( * @param node - The JSX node to walk. * @param isOnPhishingList - The function that checks the link against the * phishing list. + * @param allowedProtocols - Allowed protocols (example: ['https:']). */ export function validateJsxLinks( node: JSXElement, isOnPhishingList: (url: string) => boolean, + allowedProtocols: string[] = DEFAULT_ALLOWED_PROTOCOLS, ) { walkJsx(node, (childNode) => { if (childNode.type !== 'Link') { return; } - validateLink(childNode.props.href, isOnPhishingList); + validateLink(childNode.props.href, isOnPhishingList, allowedProtocols); }); }