diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 2cdd29161..c178c43f8 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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": { diff --git a/packages/snap-networks-utils/CHANGELOG.md b/packages/snap-networks-utils/CHANGELOG.md index b35dca48f..d1ca633e7 100644 --- a/packages/snap-networks-utils/CHANGELOG.md +++ b/packages/snap-networks-utils/CHANGELOG.md @@ -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)) diff --git a/packages/snap-networks-utils/src/index.ts b/packages/snap-networks-utils/src/index.ts index 3718c73db..b2c2a3b75 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -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 { diff --git a/packages/snap-networks-utils/src/sanitize.test.ts b/packages/snap-networks-utils/src/sanitize.test.ts new file mode 100644 index 000000000..ee4171df0 --- /dev/null +++ b/packages/snap-networks-utils/src/sanitize.test.ts @@ -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(''); + }); + }); +}); diff --git a/packages/stellar-wallet-snap/src/utils/sanitize.ts b/packages/snap-networks-utils/src/sanitize.ts similarity index 62% rename from packages/stellar-wallet-snap/src/utils/sanitize.ts rename to packages/snap-networks-utils/src/sanitize.ts index dfcaa3d6d..13f5751ca 100644 --- a/packages/stellar-wallet-snap/src/utils/sanitize.ts +++ b/packages/snap-networks-utils/src/sanitize.ts @@ -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, ''); } @@ -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') { @@ -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; diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index 124a598da..e832e7d66 100644 --- a/packages/solana-wallet-snap/snap.manifest.json +++ b/packages/solana-wallet-snap/snap.manifest.json @@ -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", diff --git a/packages/solana-wallet-snap/src/core/services/wallet/structs.ts b/packages/solana-wallet-snap/src/core/services/wallet/structs.ts index 58e5ec5c6..2687de0ce 100644 --- a/packages/solana-wallet-snap/src/core/services/wallet/structs.ts +++ b/packages/solana-wallet-snap/src/core/services/wallet/structs.ts @@ -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, @@ -21,7 +22,6 @@ import { sanitizeResources, sanitizeSolanaAddress, sanitizeTimestamp, - sanitizeUri, } from '../../utils/sanitize'; import { Base58Struct, Base64Struct } from '../../validation/structs'; diff --git a/packages/solana-wallet-snap/src/core/utils/buildUrl.ts b/packages/solana-wallet-snap/src/core/utils/buildUrl.ts index 350e5e0eb..0228b6055 100644 --- a/packages/solana-wallet-snap/src/core/utils/buildUrl.ts +++ b/packages/solana-wallet-snap/src/core/utils/buildUrl.ts @@ -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; diff --git a/packages/solana-wallet-snap/src/core/utils/sanitize.test.ts b/packages/solana-wallet-snap/src/core/utils/sanitize.test.ts index 249167e11..08d0603d8 100644 --- a/packages/solana-wallet-snap/src/core/utils/sanitize.test.ts +++ b/packages/solana-wallet-snap/src/core/utils/sanitize.test.ts @@ -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'); @@ -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( diff --git a/packages/solana-wallet-snap/src/core/utils/sanitize.ts b/packages/solana-wallet-snap/src/core/utils/sanitize.ts index b787a8733..0092566a5 100644 --- a/packages/solana-wallet-snap/src/core/utils/sanitize.ts +++ b/packages/solana-wallet-snap/src/core/utils/sanitize.ts @@ -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. @@ -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. * diff --git a/packages/stellar-wallet-snap/snap.manifest.json b/packages/stellar-wallet-snap/snap.manifest.json index 3875cbc98..399728859 100644 --- a/packages/stellar-wallet-snap/snap.manifest.json +++ b/packages/stellar-wallet-snap/snap.manifest.json @@ -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", diff --git a/packages/stellar-wallet-snap/src/utils/buildUrl.ts b/packages/stellar-wallet-snap/src/utils/buildUrl.ts index e86c34eb8..98b8b99ed 100644 --- a/packages/stellar-wallet-snap/src/utils/buildUrl.ts +++ b/packages/stellar-wallet-snap/src/utils/buildUrl.ts @@ -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'; - export type BuildUrlParams = { baseUrl: string; path: string; diff --git a/packages/stellar-wallet-snap/src/utils/index.ts b/packages/stellar-wallet-snap/src/utils/index.ts index 46f37423f..d8253e942 100644 --- a/packages/stellar-wallet-snap/src/utils/index.ts +++ b/packages/stellar-wallet-snap/src/utils/index.ts @@ -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'; diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 9421eb0ae..175050a0f 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -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", diff --git a/packages/tron-wallet-snap/src/utils/buildUrl.ts b/packages/tron-wallet-snap/src/utils/buildUrl.ts index e86c34eb8..4bdd2c4bd 100644 --- a/packages/tron-wallet-snap/src/utils/buildUrl.ts +++ b/packages/tron-wallet-snap/src/utils/buildUrl.ts @@ -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; diff --git a/packages/tron-wallet-snap/src/utils/sanitize.ts b/packages/tron-wallet-snap/src/utils/sanitize.ts deleted file mode 100644 index dfcaa3d6d..000000000 --- a/packages/tron-wallet-snap/src/utils/sanitize.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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. - */ -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, ''); -} - -/** - * 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:']; - if (!allowedProtocols.includes(url.protocol)) { - return ''; - } - if (sanitized.length > 2048) { - return ''; - } - return sanitized; - } catch { - return ''; - } -}