Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobile): prevent 3rd party access our packages #17119

Merged
merged 1 commit into from
Feb 24, 2025
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
11 changes: 11 additions & 0 deletions packages/bundler-security/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Jest configuration for web packages.
* Keeping this file next to the package.json file instead of providing configuration
* with `-c ../../jest.config.base` option in package.json scripts
* allows us to run jest tests directly from IDEs.
*/
const { ...baseConfig } = require('../../jest.config.base');

module.exports = {
...baseConfig,
};
17 changes: 17 additions & 0 deletions packages/bundler-security/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@trezor/bundler-security",
"version": "1.0.0",
"private": true,
"license": "See LICENSE.md in repo root",
"sideEffects": false,
"main": "src/index",
"scripts": {
"depcheck": "yarn g:depcheck",
"lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'",
"type-check": "yarn g:tsc --build",
"test:unit": "yarn g:jest"
},
"dependencies": {
"webpack": ">=5.0.0"
}
}
36 changes: 36 additions & 0 deletions packages/bundler-security/src/WebpackSecurityPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable no-console */
import webpack from 'webpack';

import { checkSecurityViolation } from './metroSecureResolver';

export class WebpackSecurityCheckPlugin {
private logError(message: string) {
console.error(`\x1b[1;31m${message}\x1b[0m`);
}

apply(compiler: webpack.Compiler) {
compiler.hooks.normalModuleFactory.tap('SecurityCheckPlugin', factory => {
factory.hooks.resolve.tap('SecurityCheckPlugin', resolveData => {
const { request, context, contextInfo } = resolveData;

const { isViolation, normalizedPath } = checkSecurityViolation({
moduleName: request,
originModulePath: context,
});

if (isViolation) {
console.log('\n\n');
this.logError(
`SECURITY ALERT: A third-party package is trying to import internal Trezor package! ` +
`\nRequesting module: ${request}` +
`\nNormalized path: ${normalizedPath}` +
`\nImmediate importer: ${context}` +
`\nOriginal issuer: ${contextInfo.issuer}`,
);
// Manually exit othewise webpack will only print error and happily continue building
process.exit(1);
}
});
});
}
}
11 changes: 11 additions & 0 deletions packages/bundler-security/src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const TREZOR_PACKAGES_SCOPES = ['@trezor', '@suite-common', '@suite-native'];
const PACKAGES_PATHS = ['/packages/', '/suite-common/', '/suite-native/'];

const RELATIVE_PATH_REGEX = new RegExp(
`(?:\\.\\./){2,}(?:${PACKAGES_PATHS.map(p => p.slice(1, -1)).join('|')})`,
);

module.exports = {
TREZOR_PACKAGES_SCOPES,
RELATIVE_PATH_REGEX,
};
1 change: 1 addition & 0 deletions packages/bundler-security/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { WebpackSecurityCheckPlugin } from './WebpackSecurityPlugin';
56 changes: 56 additions & 0 deletions packages/bundler-security/src/metroSecureResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Metro doesn't support TS and ES modules, so this file must be in plain JS and CommonJS
const path = require('path');

const { RELATIVE_PATH_REGEX, TREZOR_PACKAGES_SCOPES } = require('./constants');

/**
* @typedef {Object} SecurityCheckParams
* @property {string} moduleName
* @property {string} originModulePath
*/

/**
* @param {SecurityCheckParams} params
* @returns {{isViolation: boolean, normalizedPath: string}}
*/
const checkSecurityViolation = ({ moduleName, originModulePath }) => {
// Normalize path to remove some crazy relative paths like `../../scripts/..packages/connect` etc.
const normalizedPath = path.normalize(moduleName);

// Check if file is in node_modules
const isNodeModules = originModulePath.includes('node_modules');
// prevent standarts imports like `from '@trezor/connect'` or `require('@trezor/connect')` etc.
const isScopedPackage = TREZOR_PACKAGES_SCOPES.some(scope => normalizedPath.includes(scope));
// 3rd party package can import Connect from node_modules using relative path like `../../../packages/connect`
// check tests for more examples
const isRelativePath = RELATIVE_PATH_REGEX.test(normalizedPath);

return {
isViolation: (isScopedPackage || isRelativePath) && isNodeModules,
normalizedPath,
};
};

/**
* @param {SecurityCheckParams} params
*/
const metroSecureResolver = ({ moduleName, originModulePath }) => {
const { isViolation, normalizedPath } = checkSecurityViolation({
moduleName,
originModulePath,
});

if (isViolation) {
throw new Error(
`SECURITY ALERT: Some 3rd party package is trying to import internal packages or Connect!\n` +
`Module: ${moduleName}\n` +
`Normalized: ${normalizedPath}\n` +
`From file: ${originModulePath}`,
);
}
};

module.exports = {
checkSecurityViolation,
metroSecureResolver,
};
75 changes: 75 additions & 0 deletions packages/bundler-security/tests/metroSecureResolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { checkSecurityViolation } from '../src/metroSecureResolver';

describe('checkSecurityViolation', () => {
it('should detect violation when accessing @trezor scoped package from node_modules', () => {
const result = checkSecurityViolation({
moduleName: '@trezor/connect',
originModulePath: 'path/to/node_modules/some-package/index.js',
});

expect(result.isViolation).toBe(true);
});

it('should detect violation when using relative path inside node_modules', () => {
const result = checkSecurityViolation({
moduleName: '../../@trezor/connect/src/utils',
originModulePath: 'path/to/node_modules/external-lib/index.js',
});

expect(result.isViolation).toBe(true);
});

it('should detect violation when using relative path inside node_modules 2', () => {
const result = checkSecurityViolation({
moduleName: '../../../node_modules/@trezor/connect/src/utils',
originModulePath: 'path/to/node_modules/external-lib/index.js',
});

expect(result.isViolation).toBe(true);
});

it('should detect violation when using relative path from node_modules', () => {
const result = checkSecurityViolation({
moduleName: '../../../packages/connect/src/utils',
originModulePath: 'path/to/node_modules/external-lib/index.js',
});

expect(result.isViolation).toBe(true);
});

it('should detect violation when using crazy relative path from node_modules', () => {
const result = checkSecurityViolation({
moduleName: '../../scripts/../packages/connect/src/utils',
originModulePath: 'path/to/node_modules/external-lib/index.js',
});

expect(result.isViolation).toBe(true);
});

it('should allow @trezor imports from non-node_modules location', () => {
const result = checkSecurityViolation({
moduleName: '@trezor/connect',
originModulePath: 'path/to/src/components/index.ts',
});

expect(result.isViolation).toBe(false);
});

it('should allow relative imports from non-node_modules location', () => {
const result = checkSecurityViolation({
moduleName: '../utils',
originModulePath: 'path/to/src/components/index.ts',
});

expect(result.isViolation).toBe(false);
});

it('should allow regular node_modules imports', () => {
const result = checkSecurityViolation({
moduleName: 'react',
originModulePath: 'path/to/node_modules/some-package/index.js',
});

expect(result.isViolation).toBe(false);
});
});
5 changes: 5 additions & 0 deletions packages/bundler-security/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "libDev" },
"references": []
}
1 change: 1 addition & 0 deletions packages/connect-iframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"devDependencies": {
"@babel/preset-typescript": "^7.24.7",
"@trezor/bundler-security": "workspace:*",
"@trezor/env-utils": "workspace:*",
"@trezor/eslint": "workspace:*",
"babel-loader": "^9.1.3",
Expand Down
1 change: 1 addition & 0 deletions packages/connect-iframe/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{ "path": "../connect" },
{ "path": "../connect-analytics" },
{ "path": "../connect-common" },
{ "path": "../bundler-security" },
{ "path": "../env-utils" },
{ "path": "../eslint" }
]
Expand Down
3 changes: 3 additions & 0 deletions packages/connect-iframe/webpack/base.webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import webpack from 'webpack';

import { WebpackSecurityCheckPlugin } from '@trezor/bundler-security';

import { version } from '../package.json';
import { getDistPathForProject } from './utils';

Expand Down Expand Up @@ -106,6 +108,7 @@ export const config: webpack.Configuration = {
hints: false,
},
plugins: [
new WebpackSecurityCheckPlugin(),
new webpack.DefinePlugin({
'process.env.IS_CODESIGN_BUILD': `"${process.env.IS_CODESIGN_BUILD === 'true'}"`, // to keep it as string "true"/"false" and not boolean
}),
Expand Down
4 changes: 3 additions & 1 deletion packages/suite-build/configs/base.webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import webpack from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';

// Get Suite App version from the Suite package.json
import { WebpackSecurityCheckPlugin } from '@trezor/bundler-security';

import { suiteVersion } from '../../suite/package.json';
import {
assetPrefix,
Expand All @@ -16,7 +18,6 @@ import {
} from '../utils/env';
import { getRevision } from '../utils/git';
import { getPathForProject } from '../utils/path';

const gitRevision = getRevision();

/**
Expand Down Expand Up @@ -158,6 +159,7 @@ const config: webpack.Configuration = {
],
},
plugins: [
new WebpackSecurityCheckPlugin(),
new webpack.ProgressPlugin(),
new webpack.DefinePlugin({
'process.browser': true,
Expand Down
1 change: 1 addition & 0 deletions packages/suite-build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"@suite-common/suite-config": "workspace:*",
"@trezor/bundler-security": "workspace:*",
"@trezor/suite": "workspace:*",
"babel-loader": "^9.1.3",
"babel-plugin-styled-components": "^2.1.4",
Expand Down
1 change: 1 addition & 0 deletions packages/suite-build/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{
"path": "../../suite-common/suite-config"
},
{ "path": "../bundler-security" },
{ "path": "../suite" },
{ "path": "../eslint" }
]
Expand Down
8 changes: 2 additions & 6 deletions scripts/generatePackage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ import sortPackageJson from 'sort-package-json';

import templatePackageJsonWeb from './package-template/package.json';
import templatePackageJsonNative from './package-template-native/package.json';
// todo: calling yarn generate-package failed on not resolving destructuring imports. default imports seem to work.
import import1 from './utils/getPrettierConfig';
import import2 from './utils/getWorkspacesList';

const { getPrettierConfig } = import1;
const { getWorkspacesList } = import2;
import { getPrettierConfig } from './utils/getPrettierConfig';
import { getWorkspacesList } from './utils/getWorkspacesList';

const scopes = {
'@suite-common': {
Expand Down
10 changes: 9 additions & 1 deletion suite-native/app/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
const { mergeConfig } = require('@react-native/metro-config');
const { getSentryExpoConfig } = require('@sentry/react-native/metro');
const nodejs = require('node-libs-browser');

const { metroSecureResolver } = require('@trezor/bundler-security/src/metroSecureResolver');

// Learn more https://docs.expo.io/guides/customizing-metro

const jsonExpoConfig = getSentryExpoConfig(__dirname);
Expand Down Expand Up @@ -36,7 +39,12 @@ const config = {
},
sourceExts,
resolveRequest: (context, moduleName, platform) => {
// index 0 refers to suite-native/app node_modules directory
metroSecureResolver({
moduleName,
originModulePath: context.originModulePath,
});

// web3-validator package handling
const rootNodeModulesPath = context.nodeModulesPaths[1];

// web3-validator package is by default trying to use non-existing minified index file. This fixes that.
Expand Down
1 change: 1 addition & 0 deletions suite-native/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@suite-native/theme": "workspace:*",
"@suite-native/toasts": "workspace:*",
"@suite-native/transactions": "workspace:*",
"@trezor/bundler-security": "workspace:*",
"@trezor/connect": "workspace:*",
"@trezor/react-native-usb": "workspace:*",
"@trezor/styles": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions suite-native/app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
{ "path": "../theme" },
{ "path": "../toasts" },
{ "path": "../transactions" },
{
"path": "../../packages/bundler-security"
},
{ "path": "../../packages/connect" },
{
"path": "../../packages/react-native-usb"
Expand Down
4 changes: 1 addition & 3 deletions suite-native/state/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
{
"path": "../../suite-common/token-definitions"
},
{
"path": "../../suite-common/trading"
},
{ "path": "../../suite-common/trading" },
{
"path": "../../suite-common/wallet-core"
},
Expand Down
Loading
Loading