diff --git a/packages/safe-chain/src/registryProxy/certBundle.js b/packages/safe-chain/src/registryProxy/certBundle.js index 9b0c7bf..6dc9a51 100644 --- a/packages/safe-chain/src/registryProxy/certBundle.js +++ b/packages/safe-chain/src/registryProxy/certBundle.js @@ -103,7 +103,7 @@ export function getCombinedCaBundlePath() { function readUserCertificateFile(certPath) { try { // Validate path is a string and not attempting path traversal - if (typeof certPath !== "string" || certPath.includes("..") || certPath.startsWith("/")) { + if (typeof certPath !== "string" || certPath.includes("..")) { return null; } diff --git a/test/e2e/certbundle.e2e.spec.js b/test/e2e/certbundle.e2e.spec.js new file mode 100644 index 0000000..a60dc3b --- /dev/null +++ b/test/e2e/certbundle.e2e.spec.js @@ -0,0 +1,347 @@ +import { describe, it, before, beforeEach, afterEach } from "node:test"; +import { DockerTestContainer } from "./DockerTestContainer.js"; +import assert from "node:assert"; + +describe("E2E: NODE_EXTRA_CA_CERTS merging", () => { + let container; + + before(async () => { + DockerTestContainer.buildImage(); + }); + + beforeEach(async () => { + // Run a new Docker container for each test + container = new DockerTestContainer(); + await container.start(); + + const installationShell = await container.openShell("zsh"); + await installationShell.runCommand("safe-chain setup"); + }); + + afterEach(async () => { + // Stop and clean up the container after each test + if (container) { + await container.stop(); + container = null; + } + }); + + it(`npm install works without NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("zsh"); + + // Ensure NODE_EXTRA_CA_CERTS is not set + await shell.runCommand("unset NODE_EXTRA_CA_CERTS"); + + const result = await shell.runCommand("npm install axios"); + + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed without NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`npm install works with valid NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("zsh"); + + // Create a temporary valid certificate (using the system's Mozilla CA bundle) + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/valid-certs.pem"); + + // Verify the cert file was created + const { output: checkOutput } = await shell.runCommand("test -f /tmp/valid-certs.pem && echo exists"); + assert.ok( + checkOutput.includes("exists"), + `Certificate file was not created at /tmp/valid-certs.pem` + ); + + // Set NODE_EXTRA_CA_CERTS and run npm install + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/valid-certs.pem npm install axios" + ); + + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with valid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`npm install works with non-existent NODE_EXTRA_CA_CERTS path`, async () => { + const shell = await container.openShell("zsh"); + + // Set NODE_EXTRA_CA_CERTS to a non-existent path + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/nonexistent-certs.pem" && npm install axios' + ); + + // Should still succeed - safe-chain should gracefully handle missing user certs + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with non-existent NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + + // Should show a warning + assert.ok( + result.output.includes("Safe-chain") || result.output.includes("Could not read"), + `Expected safe-chain warning about missing certs. Output was:\n${result.output}` + ); + }); + + it(`npm install works with invalid (non-PEM) NODE_EXTRA_CA_CERTS`, async () => { + const shell = await container.openShell("zsh"); + + // Create an invalid certificate file (not valid PEM) + await shell.runCommand( + 'echo "This is not a valid PEM certificate" > /tmp/invalid-certs.pem' + ); + + // Set NODE_EXTRA_CA_CERTS to invalid cert + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/invalid-certs.pem" && npm install axios' + ); + + // Should still succeed - safe-chain should skip invalid user certs + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with invalid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + + // Should show a warning about invalid cert + assert.ok( + result.output.includes("Safe-chain") || result.output.includes("Could not read"), + `Expected safe-chain warning about invalid certs. Output was:\n${result.output}` + ); + }); + + it(`npm install handles NODE_EXTRA_CA_CERTS with path traversal attempt`, async () => { + const shell = await container.openShell("zsh"); + + // Try to set NODE_EXTRA_CA_CERTS with path traversal + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/../../../etc/passwd" && npm install axios' + ); + + // Should still succeed - safe-chain should reject path traversal + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with path traversal NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`npm install handles empty NODE_EXTRA_CA_CERTS`, async () => { + const shell = await container.openShell("zsh"); + + // Create an empty certificate file + await shell.runCommand("touch /tmp/empty-certs.pem"); + + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/empty-certs.pem" && npm install axios' + ); + + // Should still succeed - empty file should be ignored gracefully + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with empty NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`npm install handles NODE_EXTRA_CA_CERTS pointing to a directory`, async () => { + const shell = await container.openShell("zsh"); + + // Create a directory instead of a file + await shell.runCommand("mkdir -p /tmp/cert-dir"); + + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/cert-dir" && npm install axios' + ); + + // Should still succeed - directory should be treated as invalid cert file + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed when NODE_EXTRA_CA_CERTS points to directory. Output was:\n${result.output}` + ); + }); + + it(`npm install handles relative NODE_EXTRA_CA_CERTS path`, async () => { + const shell = await container.openShell("zsh"); + + // Create a cert file and try to reference it with relative path + await shell.runCommand( + "mkdir -p /tmp/cert-test && cp /etc/ssl/certs/ca-certificates.crt /tmp/cert-test/certs.pem" + ); + + const result = await shell.runCommand( + 'cd /tmp/cert-test && export NODE_EXTRA_CA_CERTS="./certs.pem" && npm install axios' + ); + + // Should still succeed - relative paths should be resolved properly + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with relative NODE_EXTRA_CA_CERTS path. Output was:\n${result.output}` + ); + }); + + it(`npm install handles absolute NODE_EXTRA_CA_CERTS path`, async () => { + const shell = await container.openShell("zsh"); + + // Create cert file with absolute path + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/absolute-certs.pem"); + + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/absolute-certs.pem npm install axios" + ); + + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install failed with absolute NODE_EXTRA_CA_CERTS path. Output was:\n${result.output}` + ); + }); + + it(`npm install with multiple packages still respects merged certificates`, async () => { + const shell = await container.openShell("zsh"); + + // Create valid cert + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/merge-certs.pem"); + + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/merge-certs.pem npm install axios lodash" + ); + + assert.ok( + result.output.includes("added") || result.output.includes("up to date"), + `npm install with multiple packages failed. Output was:\n${result.output}` + ); + }); + + it(`npm install correctly blocks malware even with merged certificates`, async () => { + const shell = await container.openShell("zsh"); + + // Create valid cert + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/secure-merge-certs.pem"); + + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/secure-merge-certs.pem npm install safe-chain-test" + ); + + // Should block the malware package + assert.ok( + result.output.includes("Malicious") || result.output.includes("blocked"), + `Malware package should be blocked even with merged certificates. Output was:\n${result.output}` + ); + }); + + it(`pip install works without NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("zsh"); + + await shell.runCommand("safe-chain setup --include-python"); + await shell.runCommand("unset NODE_EXTRA_CA_CERTS"); + + const result = await shell.runCommand( + "pip3 install --break-system-packages requests" + ); + + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `pip3 install failed without NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`pip install works with valid NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("zsh"); + + await shell.runCommand("safe-chain setup --include-python"); + + // Create a temporary valid certificate + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/pip-valid-certs.pem"); + + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/pip-valid-certs.pem pip3 install --break-system-packages requests" + ); + + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `pip3 install failed with valid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`pip install handles non-existent NODE_EXTRA_CA_CERTS gracefully`, async () => { + const shell = await container.openShell("zsh"); + + await shell.runCommand("safe-chain setup --include-python"); + + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/nonexistent-pip-certs.pem" && pip3 install --break-system-packages requests' + ); + + // Should still work - gracefully handle missing user certs + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `pip3 install failed with non-existent NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`pip install handles invalid NODE_EXTRA_CA_CERTS gracefully`, async () => { + const shell = await container.openShell("zsh"); + + await shell.runCommand("safe-chain setup --include-python"); + + // Create invalid cert + await shell.runCommand( + 'echo "invalid certificate content" > /tmp/pip-invalid-certs.pem' + ); + + const result = await shell.runCommand( + 'export NODE_EXTRA_CA_CERTS="/tmp/pip-invalid-certs.pem" && pip3 install --break-system-packages requests' + ); + + // Should still work - skip invalid user certs + assert.ok( + result.output.includes("Successfully installed") || result.output.includes("Requirement already satisfied"), + `pip3 install failed with invalid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`yarn install works with valid NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("zsh"); + + // Create valid cert + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/yarn-certs.pem"); + + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/yarn-certs.pem yarn add axios" + ); + + assert.ok( + result.output.includes("added"), + `yarn add failed with valid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`pnpm install works with valid NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("zsh"); + + // Create valid cert + await shell.runCommand("cp /etc/ssl/certs/ca-certificates.crt /tmp/pnpm-certs.pem"); + + const result = await shell.runCommand( + "NODE_EXTRA_CA_CERTS=/tmp/pnpm-certs.pem pnpm add axios" + ); + + assert.ok( + result.output.includes("added"), + `pnpm add failed with valid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); + + it(`bun install works with valid NODE_EXTRA_CA_CERTS set`, async () => { + const shell = await container.openShell("bash"); + + // Create valid cert and run bun in the same command to ensure file exists + const result = await shell.runCommand( + "cp /etc/ssl/certs/ca-certificates.crt /tmp/bun-certs.pem && NODE_EXTRA_CA_CERTS=/tmp/bun-certs.pem bun i axios" + ); + + assert.ok( + result.output.includes("no malware found."), + `bun i failed with valid NODE_EXTRA_CA_CERTS. Output was:\n${result.output}` + ); + }); +});