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
2 changes: 1 addition & 1 deletion eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@
},
"packages/solana-wallet-snap/src/core/utils/sanitize.test.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 14
"count": 10
}
},
"packages/solana-wallet-snap/src/core/utils/toTokenUnit.test.ts": {
Expand Down
1 change: 1 addition & 0 deletions packages/snap-networks-utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add `sanitizeControlCharacters` and `sanitizeUri` utilities for validating and sanitizing user-provided strings and URIs.
- Add a `safeMerge` utility for shallowly merging objects. ([#166](https://github.com/MetaMask/internal-snaps/pull/166))
- Add a `UrlStruct` utility for validating safe HTTP, HTTPS, and WebSocket URLs. ([#174](https://github.com/MetaMask/internal-snaps/pull/174))

Expand Down
1 change: 1 addition & 0 deletions packages/snap-networks-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
type RemoteFeatureFlagsProviderMessenger,
} from './providers/remote-feature-flags/RemoteFeatureFlagsProvider';
export { safeMerge } from './safeMerge/safeMerge';
export { sanitizeControlCharacters, sanitizeUri } from './sanitize';
export { UrlStruct } from './urlStruct/urlStruct';
export { Logger, LogLevel } from './logger';
export type {
Expand Down
54 changes: 54 additions & 0 deletions packages/snap-networks-utils/src/sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { sanitizeControlCharacters, sanitizeUri } from './sanitize';

describe('sanitize', () => {
describe('sanitizeControlCharacters', () => {
it('removes control characters from strings', () => {
expect(sanitizeControlCharacters('hello\nworld')).toBe('helloworld');
expect(sanitizeControlCharacters('hello\r\nworld')).toBe('helloworld');
// The tab character is preserved.
expect(sanitizeControlCharacters('hello\tworld')).toBe('hello\tworld');
expect(sanitizeControlCharacters('hello\x00world')).toBe('helloworld');
expect(sanitizeControlCharacters('hello\x1Fworld')).toBe('helloworld');
});

it('handles edge cases', () => {
expect(sanitizeControlCharacters('')).toBe('');
expect(sanitizeControlCharacters(null as unknown as string)).toBe('');
expect(sanitizeControlCharacters(undefined as unknown as string)).toBe(
'',
);
expect(sanitizeControlCharacters('normal text')).toBe('normal text');
});
});

describe('sanitizeUri', () => {
it('validates and sanitizes valid URIs', () => {
expect(sanitizeUri('https://example.com')).toBe('https://example.com');
expect(sanitizeUri('http://example.com/path')).toBe(
'http://example.com/path',
);
expect(sanitizeUri('wss://example.com')).toBe('wss://example.com');
expect(sanitizeUri('ipfs://example.com')).toBe('ipfs://example.com');
});

it('rejects invalid URIs', () => {
expect(sanitizeUri('')).toBe('');
expect(sanitizeUri('not-a-url')).toBe('');
expect(sanitizeUri('ftp://example.com')).toBe('');
// eslint-disable-next-line no-script-url
expect(sanitizeUri('javascript:alert(1)')).toBe('');
});

it('sanitizes URIs with control characters', () => {
expect(sanitizeUri('https://example.com\n')).toBe('https://example.com');
expect(sanitizeUri('http://example.com/path\r')).toBe(
'http://example.com/path',
);
});

it('handles edge cases', () => {
expect(sanitizeUri(null as unknown as string)).toBe('');
expect(sanitizeUri(undefined as unknown as string)).toBe('');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
/**
* Removes control characters from a string.
* Control characters can be used for injection attacks and should be stripped from user input.
*
* @param input - The string to sanitize.
* @returns The sanitized string with control characters removed.
* @returns The sanitized string.
*/
export function sanitizeControlCharacters(input: string): string {
if (!input || typeof input !== 'string') {
return '';
}

// Remove all control characters except tab
// Remove all control characters except tab.
// eslint-disable-next-line no-control-regex
return input.replace(/[\u0000-\u0008\u000A-\u001F\u007F]/gu, '');
}
Expand All @@ -19,7 +18,7 @@ export function sanitizeControlCharacters(input: string): string {
* Validates and sanitizes a URI.
*
* @param uri - The URI to validate and sanitize.
* @returns The sanitized URI or empty string if invalid.
* @returns The sanitized URI or an empty string if invalid.
*/
export function sanitizeUri(uri: string): string {
if (!uri || typeof uri !== 'string') {
Expand All @@ -30,11 +29,8 @@ export function sanitizeUri(uri: string): string {

try {
const url = new URL(sanitized);
const allowedProtocols = ['http:', 'https:', 'wss:'];
if (!allowedProtocols.includes(url.protocol)) {
return '';
}
if (sanitized.length > 2048) {
const allowedProtocols = ['http:', 'https:', 'wss:', 'ipfs:'];
if (!allowedProtocols.includes(url.protocol) || sanitized.length > 2048) {
return '';
}
return sanitized;
Expand Down
2 changes: 1 addition & 1 deletion packages/solana-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/internal-snaps.git"
},
"source": {
"shasum": "lJVS123VPh0Ye8zVmLFMCd0tRpKNnV9bGEJ8mKjoYQs=",
"shasum": "lKgpeQgMXQzAgQ8GpHxIQ0ot/jc+D5HIZhrccB9fUjI=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SolMethod } from '@metamask/keyring-api';
import { sanitizeUri } from '@metamask/snap-networks-utils';
import type { Infer } from '@metamask/superstruct';
import {
array,
Expand All @@ -21,7 +22,6 @@ import {
sanitizeResources,
sanitizeSolanaAddress,
sanitizeTimestamp,
sanitizeUri,
} from '../../utils/sanitize';
import { Base58Struct, Base64Struct } from '../../validation/structs';

Expand Down
8 changes: 5 additions & 3 deletions packages/solana-wallet-snap/src/core/utils/buildUrl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UrlStruct } from '@metamask/snap-networks-utils';
import {
sanitizeControlCharacters,
sanitizeUri,
UrlStruct,
} from '@metamask/snap-networks-utils';
import { assert } from '@metamask/superstruct';

import { sanitizeControlCharacters, sanitizeUri } from './sanitize';

export type BuildUrlParams = {
baseUrl: string;
path: string;
Expand Down
50 changes: 0 additions & 50 deletions packages/solana-wallet-snap/src/core/utils/sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
import {
sanitizeControlCharacters,
sanitizeForSignInMessage,
sanitizeDomain,
sanitizeSolanaAddress,
sanitizeUri,
sanitizeTimestamp,
sanitizeResources,
} from './sanitize';

describe('sanitize', () => {
describe('sanitizeControlCharacters', () => {
it('removes control characters from strings', () => {
expect(sanitizeControlCharacters('hello\nworld')).toBe('helloworld');
expect(sanitizeControlCharacters('hello\r\nworld')).toBe('helloworld');
// The tab character is preserved
expect(sanitizeControlCharacters('hello\tworld')).toBe('hello\tworld');
expect(sanitizeControlCharacters('hello\x00world')).toBe('helloworld');
expect(sanitizeControlCharacters('hello\x1Fworld')).toBe('helloworld');
});

it('handles edge cases', () => {
expect(sanitizeControlCharacters('')).toBe('');
expect(sanitizeControlCharacters(null as any)).toBe('');
expect(sanitizeControlCharacters(undefined as any)).toBe('');
expect(sanitizeControlCharacters('normal text')).toBe('normal text');
});
});

describe('sanitizeForSignInMessage', () => {
it('sanitizes strings for sign-in messages', () => {
expect(sanitizeForSignInMessage('hello\nworld')).toBe('helloworld');
Expand Down Expand Up @@ -106,36 +86,6 @@ describe('sanitize', () => {
});
});

describe('sanitizeUri', () => {
it('validates and sanitizes valid URIs', () => {
expect(sanitizeUri('https://example.com')).toBe('https://example.com');
expect(sanitizeUri('http://example.com/path')).toBe(
'http://example.com/path',
);
expect(sanitizeUri('wss://example.com')).toBe('wss://example.com');
});

it('rejects invalid URIs', () => {
expect(sanitizeUri('')).toBe('');
expect(sanitizeUri('not-a-url')).toBe('');
expect(sanitizeUri('ftp://example.com')).toBe('');
// eslint-disable-next-line no-script-url
expect(sanitizeUri('javascript:alert(1)')).toBe('');
});

it('sanitizes URIs with control characters', () => {
expect(sanitizeUri('https://example.com\n')).toBe('https://example.com');
expect(sanitizeUri('http://example.com/path\r')).toBe(
'http://example.com/path',
);
});

it('handles edge cases', () => {
expect(sanitizeUri(null as any)).toBe('');
expect(sanitizeUri(undefined as any)).toBe('');
});
});

describe('sanitizeTimestamp', () => {
it('validates and sanitizes valid timestamps', () => {
expect(sanitizeTimestamp('2024-01-01T00:00:00.000Z')).toBe(
Expand Down
47 changes: 4 additions & 43 deletions packages/solana-wallet-snap/src/core/utils/sanitize.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
/**
* Removes control characters from a string.
*
* @param input - The string to sanitize.
* @returns The sanitized string.
*/
export function sanitizeControlCharacters(input: string): string {
if (!input || typeof input !== 'string') {
return '';
}

// Remove all control characters except tab
// eslint-disable-next-line no-control-regex
return input.replace(/[\u0000-\u0008\u000A-\u001F\u007F]/gu, '');
}
import {
sanitizeControlCharacters,
sanitizeUri,
} from '@metamask/snap-networks-utils';

/**
* Sanitizes a string for use in sign-in messages.
Expand Down Expand Up @@ -113,34 +102,6 @@ export function sanitizeSolanaAddress(address: string): string {
return sanitized;
}

/**
* Validates and sanitizes a URI.
*
* @param uri - The URI to validate and sanitize.
* @returns The sanitized URI or empty string if invalid.
*/
export function sanitizeUri(uri: string): string {
if (!uri || typeof uri !== 'string') {
return '';
}

const sanitized = sanitizeControlCharacters(uri);

try {
const url = new URL(sanitized);
const allowedProtocols = ['http:', 'https:', 'wss:', 'ipfs:'];
if (!allowedProtocols.includes(url.protocol)) {
return '';
}
if (sanitized.length > 2048) {
return '';
}
return sanitized;
} catch {
return '';
}
}

/**
* Validates and sanitizes a timestamp string.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/stellar-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/internal-snaps.git"
},
"source": {
"shasum": "5rIjjg4hOY1vdlUoOn/JFTXDh+z7PRmb5ViKFFs4phQ=",
"shasum": "sPQUAKKjPYOF2qH5DqQPbUhcmS9H0oma0X30v/5jO44=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
8 changes: 5 additions & 3 deletions packages/stellar-wallet-snap/src/utils/buildUrl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UrlStruct } from '@metamask/snap-networks-utils';
import {
UrlStruct,
sanitizeControlCharacters,
sanitizeUri,
} from '@metamask/snap-networks-utils';
import { assert } from '@metamask/superstruct';

import { sanitizeControlCharacters, sanitizeUri } from './sanitize';
Comment thread
taran-a marked this conversation as resolved.

export type BuildUrlParams = {
baseUrl: string;
path: string;
Expand Down
1 change: 0 additions & 1 deletion packages/stellar-wallet-snap/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export * from './number';
export * from './caip';
export * from './buffer';
export * from './buildUrl';
export * from './sanitize';
export * from './async';
export * from './assert';
export * from './array';
Expand Down
2 changes: 1 addition & 1 deletion packages/tron-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/internal-snaps.git"
},
"source": {
"shasum": "jQoMKntoaRUTbeHrxShyHZrOe3GayeA/eavtjIP2BbI=",
"shasum": "DNzPDkBCR5cTTuahlNeSgrYrXNzOl8ebz9iklL/zXSg=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
8 changes: 5 additions & 3 deletions packages/tron-wallet-snap/src/utils/buildUrl.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UrlStruct } from '@metamask/snap-networks-utils';
import {
sanitizeControlCharacters,
sanitizeUri,
UrlStruct,
} from '@metamask/snap-networks-utils';
import { assert } from '@metamask/superstruct';

import { sanitizeControlCharacters, sanitizeUri } from './sanitize';

export type BuildUrlParams = {
baseUrl: string;
path: string;
Expand Down
44 changes: 0 additions & 44 deletions packages/tron-wallet-snap/src/utils/sanitize.ts

This file was deleted.