diff --git a/packages/safe-chain/src/config/settings.js b/packages/safe-chain/src/config/settings.js index 47c98c4d..6f37ba2a 100644 --- a/packages/safe-chain/src/config/settings.js +++ b/packages/safe-chain/src/config/settings.js @@ -200,6 +200,25 @@ export function getMinimumPackageAgeExclusions() { return [...new Set(allExclusions)]; } +/** + * Masks credentials in a URL for logging purposes. + * @param {string} url + * @returns {string} + */ +function maskCredentialsInUrl(url) { + if (!url || typeof url !== "string") { + return url; + } + + // Mask credentials https://username:password@abc.example or https://username@abc.example by replacing with https://***@abc.example + let masked = url.replace(/:\/\/([^@]+)@/, "://***@"); + + // Remove control characters to prevent log poisoning + masked = masked.replace(/[\x00-\x1F\x7F]/g, ""); + + return masked; +} + /** * Gets the malware list base URL with priority: CLI argument > environment variable > config file > default * @returns {string} @@ -209,7 +228,7 @@ export function getMalwareListBaseUrl() { const cliValue = cliArguments.getMalwareListBaseUrl(); if (cliValue) { const url = removeTrailingSlashes(cliValue); - ui.writeInformation(`Fetching malware lists from ${url} as defined by CLI argument --safe-chain-malware-list-base-url`); + ui.writeInformation(`Fetching malware lists from ${maskCredentialsInUrl(url)} as defined by CLI argument --safe-chain-malware-list-base-url`); return url; } @@ -217,7 +236,7 @@ export function getMalwareListBaseUrl() { const envValue = environmentVariables.getMalwareListBaseUrl(); if (envValue) { const url = removeTrailingSlashes(envValue); - ui.writeInformation(`Fetching malware lists from ${url} as defined by environment variable SAFE_CHAIN_MALWARE_LIST_BASE_URL`); + ui.writeInformation(`Fetching malware lists from ${maskCredentialsInUrl(url)} as defined by environment variable SAFE_CHAIN_MALWARE_LIST_BASE_URL`); return url; } @@ -225,13 +244,13 @@ export function getMalwareListBaseUrl() { const configValue = configFile.getMalwareListBaseUrl(); if (configValue) { const url = removeTrailingSlashes(configValue); - ui.writeInformation(`Fetching malware lists from ${url} as defined by config file (malwareListBaseUrl)`); + ui.writeInformation(`Fetching malware lists from ${maskCredentialsInUrl(url)} as defined by config file (malwareListBaseUrl)`); return url; } // Default const url = removeTrailingSlashes("https://malware-list.aikido.dev"); - ui.writeInformation(`Fetching malware lists from ${url} (default)`); + ui.writeInformation(`Fetching malware lists from ${maskCredentialsInUrl(url)} (default)`); return url; } diff --git a/packages/safe-chain/src/config/settings.spec.js b/packages/safe-chain/src/config/settings.spec.js index 48108c4e..463395a4 100644 --- a/packages/safe-chain/src/config/settings.spec.js +++ b/packages/safe-chain/src/config/settings.spec.js @@ -2,6 +2,7 @@ import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; let configFileContent = undefined; +let loggedMessages = []; mock.module("fs", { namedExports: { existsSync: () => configFileContent !== undefined, @@ -11,6 +12,14 @@ mock.module("fs", { }, }); +mock.module("../environment/userInteraction.js", { + namedExports: { + ui: { + writeInformation: (message) => loggedMessages.push(message), + }, + }, +}); + const { getNpmCustomRegistries, getPipCustomRegistries, @@ -545,6 +554,7 @@ describe("getMalwareListBaseUrl", () => { delete process.env[envVarName]; // Reset CLI arguments state initializeCliArguments([]); + loggedMessages = []; }); afterEach(() => { @@ -644,4 +654,46 @@ describe("getMalwareListBaseUrl", () => { assert.strictEqual(url, "https://cli-mirror.com"); }); + + it("should mask credentials in logged URL from CLI argument", () => { + initializeCliArguments(["--safe-chain-malware-list-base-url=https://user:pass@cli-mirror.com"]); + + const url = getMalwareListBaseUrl(); + + assert.strictEqual(url, "https://user:pass@cli-mirror.com"); + assert.strictEqual(loggedMessages.length, 1); + assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@cli-mirror.com as defined by CLI argument --safe-chain-malware-list-base-url"); + }); + + it("should mask credentials in logged URL from environment variable", () => { + process.env[envVarName] = "https://user:pass@env-mirror.com"; + + const url = getMalwareListBaseUrl(); + + assert.strictEqual(url, "https://user:pass@env-mirror.com"); + assert.strictEqual(loggedMessages.length, 1); + assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@env-mirror.com as defined by environment variable SAFE_CHAIN_MALWARE_LIST_BASE_URL"); + }); + + it("should mask credentials in logged URL from config file", () => { + configFileContent = JSON.stringify({ + malwareListBaseUrl: "https://user:pass@config-mirror.com", + }); + + const url = getMalwareListBaseUrl(); + + assert.strictEqual(url, "https://user:pass@config-mirror.com"); + assert.strictEqual(loggedMessages.length, 1); + assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@config-mirror.com as defined by config file (malwareListBaseUrl)"); + }); + + it("should sanitize control characters in logged URL", () => { + initializeCliArguments(["--safe-chain-malware-list-base-url=https://user:pass@cli-mirror.com\nmalicious"]); + + const url = getMalwareListBaseUrl(); + + assert.strictEqual(url, "https://user:pass@cli-mirror.com\nmalicious"); + assert.strictEqual(loggedMessages.length, 1); + assert.strictEqual(loggedMessages[0], "Fetching malware lists from https://***@cli-mirror.commalicious as defined by CLI argument --safe-chain-malware-list-base-url"); + }); });