Skip to content

Commit be14654

Browse files
committed
Validate local snap URL protocols
1 parent 75596e0 commit be14654

File tree

4 files changed

+85
-14
lines changed

4 files changed

+85
-14
lines changed

packages/snaps-controllers/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const baseConfig = require('../../jest.config.base');
55
module.exports = deepmerge(baseConfig, {
66
coverageThreshold: {
77
global: {
8-
branches: 88.85,
8+
branches: 89.25,
99
functions: 96.58,
1010
lines: 96.74,
1111
statements: 96.74,

packages/snaps-controllers/src/snaps/SnapController.test.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2282,6 +2282,49 @@ describe('SnapController', () => {
22822282
);
22832283
});
22842284

2285+
it.each([
2286+
'local:foo',
2287+
'local:foo://localhost:8080',
2288+
'local:http://foo:8080',
2289+
'local:https://foo:8080',
2290+
])(
2291+
'returns an error on invalid local snap URL in id',
2292+
async (invalidLocalId) => {
2293+
const messenger = getSnapControllerMessenger();
2294+
const controller = getSnapController(
2295+
getSnapControllerOptions({ messenger }),
2296+
);
2297+
2298+
const callActionMock = jest
2299+
.spyOn(messenger, 'call')
2300+
.mockImplementation((method, ..._args: unknown[]) => {
2301+
if (method === 'PermissionController:hasPermission') {
2302+
return true;
2303+
}
2304+
2305+
return false;
2306+
});
2307+
2308+
const result = await controller.installSnaps(MOCK_ORIGIN, {
2309+
[invalidLocalId]: {},
2310+
});
2311+
2312+
expect(result).toStrictEqual({
2313+
[invalidLocalId]: {
2314+
error: expect.objectContaining({
2315+
message: expect.stringMatching(/^Invalid URL:/iu),
2316+
}),
2317+
},
2318+
});
2319+
expect(callActionMock).toHaveBeenCalledTimes(1);
2320+
expect(callActionMock).toHaveBeenCalledWith(
2321+
'PermissionController:hasPermission',
2322+
MOCK_ORIGIN,
2323+
expect.anything(),
2324+
);
2325+
},
2326+
);
2327+
22852328
it('updates a snap', async () => {
22862329
const newVersion = '1.0.2';
22872330
const newVersionRange = '>=1.0.1';
@@ -2507,11 +2550,19 @@ describe('SnapController', () => {
25072550
});
25082551

25092552
describe('updateSnap', () => {
2510-
it('throws an error on invalid snap id', async () => {
2511-
await expect(async () =>
2512-
getSnapController().updateSnap(MOCK_ORIGIN, 'local:foo'),
2513-
).rejects.toThrow('Snap "local:foo" not found');
2514-
});
2553+
it.each([
2554+
'local:foo',
2555+
'local:foo://localhost:8080',
2556+
'local:http://foo:8080',
2557+
'local:https://foo:8080',
2558+
])(
2559+
'throws an error on invalid local snap URL in id',
2560+
async (invalidLocalId) => {
2561+
await expect(async () =>
2562+
getSnapController().updateSnap(MOCK_ORIGIN, invalidLocalId as any),
2563+
).rejects.toThrow(`Snap "${invalidLocalId}" not found`);
2564+
},
2565+
);
25152566

25162567
it('throws an error if the specified SemVer range is invalid', async () => {
25172568
const controller = getSnapController(

packages/snaps-controllers/src/snaps/SnapController.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
InstallSnapsResult,
3131
isValidSemVerRange,
3232
LOCALHOST_HOSTNAMES,
33+
LOCALHOST_PROTOCOLS,
3334
NpmSnapFileNames,
3435
PersistedSnap,
3536
ProcessSnapResult,
@@ -2069,21 +2070,28 @@ export class SnapController extends BaseController<
20692070
/**
20702071
* Fetches the manifest and source code of a local snap.
20712072
*
2072-
* @param localhostUrl - The localhost URL to download from.
2073+
* @param localhostUrlString - The localhost URL to download from.
20732074
* @returns The validated manifest and the source code.
20742075
*/
2075-
async #fetchLocalSnap(localhostUrl: string): Promise<FetchSnapResult> {
2076+
async #fetchLocalSnap(localhostUrlString: string): Promise<FetchSnapResult> {
2077+
const localhostUrl = new URL(localhostUrlString);
2078+
if (
2079+
!LOCALHOST_PROTOCOLS.has(localhostUrl.protocol) ||
2080+
!LOCALHOST_HOSTNAMES.has(localhostUrl.hostname)
2081+
) {
2082+
throw new Error(
2083+
`Invalid URL: Locally hosted snaps must be hosted on localhost via one of the following protocols [ ${Array.from(
2084+
LOCALHOST_PROTOCOLS,
2085+
).join(', ')} ]. Received URL: "${localhostUrl.toString()}"`,
2086+
);
2087+
}
2088+
20762089
// Local snaps are mostly used for development purposes. Fetches were cached in the browser and were not requested
20772090
// afterwards which lead to confusing development where old versions of snaps were installed.
20782091
// Thus we disable caching
20792092
const fetchOptions: RequestInit = { cache: 'no-cache' };
2080-
const manifestUrl = new URL(NpmSnapFileNames.Manifest, localhostUrl);
2081-
if (!LOCALHOST_HOSTNAMES.has(manifestUrl.hostname)) {
2082-
throw new Error(
2083-
`Invalid URL: Locally hosted Snaps must be hosted on localhost. Received URL: "${manifestUrl.toString()}"`,
2084-
);
2085-
}
20862093

2094+
const manifestUrl = new URL(NpmSnapFileNames.Manifest, localhostUrl);
20872095
const manifest = await (
20882096
await this.#fetchFunction(manifestUrl.toString(), fetchOptions)
20892097
).json();

packages/snaps-utils/src/snaps.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ import { SnapManifest, SnapPermissions } from './manifest/validation';
77
import { SnapId, SnapIdPrefixes, SnapValidationFailureReason } from './types';
88
import { SemVerVersion } from './versions';
99

10+
/**
11+
* Supported URL protocols for locally hosted snaps.
12+
*/
13+
export const LOCALHOST_PROTOCOLS = new Set(['http:', 'https:']);
14+
15+
/**
16+
* Supported URL hostnames for locally hosted snaps.
17+
*/
1018
export const LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
19+
20+
/**
21+
* The prefix of the Snap restricted RPC method / permission.
22+
*/
1123
export const SNAP_PREFIX = 'wallet_snap_';
1224

1325
export const SNAP_PREFIX_REGEX = new RegExp(`^${SNAP_PREFIX}`, 'u');

0 commit comments

Comments
 (0)