Skip to content
Open
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
27 changes: 23 additions & 4 deletions packages/safe-chain/src/config/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -209,29 +228,29 @@ 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;
}

// Priority 2: Environment variable
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;
}

// Priority 3: Config file
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;
}

Expand Down
52 changes: 52 additions & 0 deletions packages/safe-chain/src/config/settings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -11,6 +12,14 @@ mock.module("fs", {
},
});

mock.module("../environment/userInteraction.js", {
namedExports: {
ui: {
writeInformation: (message) => loggedMessages.push(message),
},
},
});

const {
getNpmCustomRegistries,
getPipCustomRegistries,
Expand Down Expand Up @@ -545,6 +554,7 @@ describe("getMalwareListBaseUrl", () => {
delete process.env[envVarName];
// Reset CLI arguments state
initializeCliArguments([]);
loggedMessages = [];
});

afterEach(() => {
Expand Down Expand Up @@ -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");
});
});