Skip to content

Commit

Permalink
feat(build): add security check to build
Browse files Browse the repository at this point in the history
  • Loading branch information
Nodonisko committed Feb 21, 2025
1 parent 198d6f4 commit a6ce364
Show file tree
Hide file tree
Showing 20 changed files with 257 additions and 19 deletions.
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

0 comments on commit a6ce364

Please sign in to comment.