diff --git a/e2e/esbuild/src/esbuild.test.ts b/e2e/esbuild/src/esbuild.test.ts index 5ed6b47f3e7401..e0a4f6abc3afd6 100644 --- a/e2e/esbuild/src/esbuild.test.ts +++ b/e2e/esbuild/src/esbuild.test.ts @@ -230,4 +230,32 @@ describe('EsBuild Plugin', () => { const output = runCLI(`build ${myPkg}`); expect(output).toContain('custom config loaded'); }, 120_000); + + it('should bundle in non-sensitive NX_ environment variables', () => { + const myPkg = uniq('my-pkg'); + runCLI(`generate @nx/js:lib ${myPkg} --bundler=esbuild`, {}); + + updateFile( + `libs/${myPkg}/src/index.ts`, + ` + console.log(process.env['NX_CLOUD_ENCRYPTION_KEY']); + console.log(process.env['NX_CLOUD_ACCESS_TOKEN']); + console.log(process.env['NX_PUBLIC_TEST']); + ` + ); + + runCLI(`build ${myPkg} --platform=browser`, { + env: { + NX_CLOUD_ENCRYPTION_KEY: 'secret', + NX_CLOUD_ACCESS_TOKEN: 'secret', + NX_PUBLIC_TEST: 'foobar', + }, + }); + + const output = runCommand(`node dist/libs/${myPkg}/index.cjs`, { + failOnError: true, + }); + expect(output).not.toMatch(/secret/); + expect(output).toMatch(/foobar/); + }); }); diff --git a/e2e/storybook/src/storybook.test.ts b/e2e/storybook/src/storybook.test.ts index 0e34fb9803185e..bc0b91a16cba81 100644 --- a/e2e/storybook/src/storybook.test.ts +++ b/e2e/storybook/src/storybook.test.ts @@ -2,11 +2,14 @@ import { checkFilesExist, cleanupProject, killPorts, + listFiles, newProject, + readFile, runCLI, runCommandUntil, tmpProjPath, uniq, + updateFile, } from '@nx/e2e/utils'; import { writeFileSync } from 'fs'; import { createFileSync } from 'fs-extra'; @@ -106,5 +109,37 @@ describe('Storybook generators and executors for monorepos', () => { runCLI(`run ${reactStorybookApp}:build-storybook --verbose`); checkFilesExist(`${reactStorybookApp}/storybook-static/index.html`); }, 300_000); + + it('should bundle in non-sensitive NX_ environment variables', () => { + updateFile( + `${reactStorybookApp}/.storybook/main.ts`, + (content) => ` + ${content} + console.log(process.env); + ` + ); + runCLI(`run ${reactStorybookApp}:build-storybook --verbose`, { + env: { + NX_CLOUD_ENCRYPTION_KEY: 'secret', + NX_CLOUD_ACCESS_TOKEN: 'secret', + NX_PUBLIC_TEST: 'foobar', + }, + }); + + // Check all output chunks for bundled environment variables + const outDir = `${reactStorybookApp}/storybook-static`; + const files = listFiles(outDir); + let nxPublicKeyFound = false; + for (const file of files) { + if (!file.endsWith('.js')) continue; + const content = readFile(`${outDir}/${file}`); + expect(content).not.toMatch(/NX_CLOUD_ENCRYPTION_KEY/); + expect(content).not.toMatch(/NX_CLOUD_ACCESS_TOKEN/); + if (content.match(/NX_PUBLIC_TEST/)) { + nxPublicKeyFound = true; + } + } + expect(nxPublicKeyFound).toBe(true); + }, 300_000); }); }); diff --git a/e2e/webpack/src/webpack.test.ts b/e2e/webpack/src/webpack.test.ts index 9837e29e1efa15..e538e6d31c1cb8 100644 --- a/e2e/webpack/src/webpack.test.ts +++ b/e2e/webpack/src/webpack.test.ts @@ -1,7 +1,8 @@ import { - checkFilesExist, cleanupProject, + listFiles, newProject, + readFile, rmDist, runCLI, runCommand, @@ -159,4 +160,32 @@ describe('Webpack Plugin', () => { let output = runCommand(`node dist/${appName}/main.js`); expect(output).toMatch(/Hello/); }, 500_000); + + it('should bundle in non-sensitive NX_ environment variables', () => { + const appName = uniq('app'); + runCLI(`generate @nx/web:app ${appName} --bundler webpack`); + updateFile( + `apps/${appName}/src/main.ts`, + ` + console.log(process.env['NX_CLOUD_ENCRYPTION_KEY']); + console.log(process.env['NX_CLOUD_ACCESS_TOKEN']); + console.log(process.env['NX_PUBLIC_TEST']); + ` + ); + + runCLI(`build ${appName}`, { + env: { + NX_CLOUD_ENCRYPTION_KEY: 'secret', + NX_CLOUD_ACCESS_TOKEN: 'secret', + NX_PUBLIC_TEST: 'foobar', + }, + }); + + const mainFile = listFiles(`dist/apps/${appName}`).filter((f) => + f.startsWith('main.') + ); + const content = readFile(`dist/apps/${appName}/${mainFile}`); + expect(content).not.toMatch(/secret/); + expect(content).toMatch(/foobar/); + }); }); diff --git a/packages/esbuild/src/utils/environment-variables.ts b/packages/esbuild/src/utils/environment-variables.ts index 62ecdad9b5381c..e901291198e17c 100644 --- a/packages/esbuild/src/utils/environment-variables.ts +++ b/packages/esbuild/src/utils/environment-variables.ts @@ -1,8 +1,15 @@ +// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`). +// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18. +const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY']; + export function getClientEnvironment(): Record { const NX_APP = /^NX_/i; return Object.keys(process.env) - .filter((key) => NX_APP.test(key) || key === 'NODE_ENV') + .filter( + (key) => + !excludedKeys.includes(key) && (NX_APP.test(key) || key === 'NODE_ENV') + ) .reduce((env, key) => { env[`process.env.${key}`] = JSON.stringify(process.env[key]); return env; diff --git a/packages/next/plugins/with-nx.ts b/packages/next/plugins/with-nx.ts index 5d4d3a4aa3bed5..3d9a7e5cde8be3 100644 --- a/packages/next/plugins/with-nx.ts +++ b/packages/next/plugins/with-nx.ts @@ -360,9 +360,13 @@ export function getNextConfig( }; } +// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`). +// TODO(v19): BREAKING: Only support NEXT_PUBLIC_ env vars and ignore NX_ vars since this is a standard Next.js feature. +const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY']; + function getNxEnvironmentVariables() { return Object.keys(process.env) - .filter((env) => /^NX_/i.test(env)) + .filter((env) => !excludedKeys.includes(env) && /^NX_/i.test(env)) .reduce((env, key) => { env[key] = process.env[key]; return env; diff --git a/packages/react/plugins/storybook/index.ts b/packages/react/plugins/storybook/index.ts index 1835e812b3e4c8..f8f57a99104e36 100644 --- a/packages/react/plugins/storybook/index.ts +++ b/packages/react/plugins/storybook/index.ts @@ -18,6 +18,10 @@ import { mergePlugins } from './merge-plugins'; import { withReact } from '../with-react'; import { existsSync } from 'fs'; +// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`). +// TODO(v19): BREAKING: Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18. +const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY']; + // This is shamelessly taken from CRA and modified for NX use // https://github.com/facebook/create-react-app/blob/4784997f0682e75eb32a897b4ffe34d735912e6c/packages/react-scripts/config/env.js#L71 function getClientEnvironment(mode) { @@ -27,7 +31,11 @@ function getClientEnvironment(mode) { const STORYBOOK_PREFIX = /^STORYBOOK_/i; const raw = Object.keys(process.env) - .filter((key) => NX_PREFIX.test(key) || STORYBOOK_PREFIX.test(key)) + .filter( + (key) => + !excludedKeys.includes(key) && + (NX_PREFIX.test(key) || STORYBOOK_PREFIX.test(key)) + ) .reduce( (env, key) => { env[key] = process.env[key]; diff --git a/packages/webpack/src/utils/get-client-environment.ts b/packages/webpack/src/utils/get-client-environment.ts index 174a22a04b8736..db5f3e6694efcc 100644 --- a/packages/webpack/src/utils/get-client-environment.ts +++ b/packages/webpack/src/utils/get-client-environment.ts @@ -1,10 +1,14 @@ +// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`). +// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18. +const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY']; + export function getClientEnvironment(mode?: string) { // Grab NODE_ENV and NX_* environment variables and prepare them to be // injected into the application via DefinePlugin in webpack configuration. const NX_APP = /^NX_/i; const raw = Object.keys(process.env) - .filter((key) => NX_APP.test(key)) + .filter((key) => !excludedKeys.includes(key) && NX_APP.test(key)) .reduce( (env, key) => { env[key] = process.env[key]; diff --git a/packages/webpack/src/utils/webpack/interpolate-env-variables-to-index.ts b/packages/webpack/src/utils/webpack/interpolate-env-variables-to-index.ts index f942098020478d..2db35b8615766c 100644 --- a/packages/webpack/src/utils/webpack/interpolate-env-variables-to-index.ts +++ b/packages/webpack/src/utils/webpack/interpolate-env-variables-to-index.ts @@ -8,8 +8,12 @@ export function interpolateEnvironmentVariablesToIndex( const NX_PREFIX = /^NX_/i; +// Prevent sensitive keys from being bundled when source code uses entire `process.env` object rather than individual keys (e.g. `process.env.NX_FOO`). +// TODO(v19): Only env vars prefixed with NX_PUBLIC should be bundled. This is a breaking change so we won't do it in v18. +const excludedKeys = ['NX_CLOUD_ACCESS_TOKEN', 'NX_CLOUD_ENCRYPTION_KEY']; + function isNxEnvironmentKey(x: string): boolean { - return NX_PREFIX.test(x); + return !excludedKeys.includes(x) && NX_PREFIX.test(x); } function getClientEnvironment(deployUrl: string) {