@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
311 lines (288 loc) • 12.3 kB
JavaScript
import process from "node:process";
import { recordEnvironmentRead } from "../../helpers/utils.js";
const PERMISSION_FLAGS = [
"--permission",
"--allow-fs-read",
"--allow-fs-write",
"--allow-child-process",
"--allow-addons",
"--allow-worker",
"--allow-net",
"--allow-env",
"--allow-wasi",
];
// Flags that allow arbitrary code execution or debugger attachment when set via NODE_OPTIONS.
const CODE_EXECUTION_PATTERNS = [
/--require\b/i,
/--eval\b/i,
/--print\b/i,
/--import\b/i,
/--loader\b/i,
/--inspect(-brk)?\b/i,
/--env-file\b/i,
];
// JVM flags that allow class/agent injection.
const JVM_CODE_EXECUTION_PATTERNS = [
/-javaagent\b/i,
/-agentlib\b/i,
/-agentpath\b/i,
/-Djdk\.module\.illegalAccess/i,
/--add-opens\b/i,
];
// Environment variables whose mere presence (with any non-empty value) signals a risk.
const RISKY_PRESENCE_VARS = [
"NODE_PATH",
"NODE_NO_WARNINGS",
"NODE_PENDING_DEPRECATION",
"UV_THREADPOOL_SIZE",
];
// Pattern to detect environment variables that likely contain credentials.
// Uses an end-of-string anchor ($) so that common system variables like
// SSH_AUTH_SOCK (ends with _SOCK) and __CF_USER_TEXT_ENCODING (ends with _ENCODING)
// are NOT flagged as false positives.
// `cred` is kept alongside `credential(?:s)?` to also catch short-form names like MY_CRED.
const CREDENTIAL_VAR_PATTERN =
/_(?:token|key|secret|pass(?:word)?|credential(?:s)?|cred|user|email|auth|session)$/i;
// Proxy variables — NO_PROXY is a bypass-list and should not trigger a finding on its own.
const PROXY_VARS = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"];
const PERMISSION_FLAG_PATTERNS = PERMISSION_FLAGS.map(
(f) => new RegExp(`${f.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i"),
);
export function auditEnvironment(env = process.env) {
const findings = [];
const envSource = env === process.env ? "process.env" : "env";
const readEnv = (varName) => {
recordEnvironmentRead(varName, { source: envSource });
return env[varName];
};
const nodeOptions = readEnv("NODE_OPTIONS") || "";
const cdxgenNodeOptions = readEnv("CDXGEN_NODE_OPTIONS") || "";
const hasPermission = PERMISSION_FLAG_PATTERNS.some((re) =>
re.test(nodeOptions),
);
const cdxgenHasPermission = PERMISSION_FLAG_PATTERNS.some((re) =>
re.test(cdxgenNodeOptions),
);
const cdxgenHasCodeExecutionRisk = CODE_EXECUTION_PATTERNS.some((re) =>
re.test(cdxgenNodeOptions),
);
if (cdxgenHasPermission) {
findings.push({
type: "environment-variable",
variable: "CDXGEN_NODE_OPTIONS",
severity: "high",
message:
"CDXGEN_NODE_OPTIONS enables Node.js permission flags. These flags can alter filesystem, network, environment, worker, or child-process access during cdxgen execution.",
mitigation:
"Remove permission-related flags from CDXGEN_NODE_OPTIONS unless they are explicitly required and reviewed for safety.",
});
}
if (cdxgenHasCodeExecutionRisk) {
findings.push({
type: "environment-variable",
variable: "CDXGEN_NODE_OPTIONS",
severity: "high",
message:
"CDXGEN_NODE_OPTIONS contains Node.js flags that can inject code, load arbitrary modules, read env files, or enable debugger attachment.",
mitigation:
"Unset CDXGEN_NODE_OPTIONS or remove flags such as --require, --import, --loader, --eval, --print, --inspect, or --env-file.",
});
}
// NODE_TLS_REJECT_UNAUTHORIZED=0 disables TLS verification; any other value is benign.
if (readEnv("NODE_TLS_REJECT_UNAUTHORIZED") === "0") {
findings.push({
type: "environment-variable",
variable: "NODE_TLS_REJECT_UNAUTHORIZED",
severity: "high",
message:
"TLS certificate verification is disabled globally (NODE_TLS_REJECT_UNAUTHORIZED=0). All HTTPS connections, including SBOM uploads, are vulnerable to interception.",
mitigation:
"Unset NODE_TLS_REJECT_UNAUTHORIZED or set it to '1'. Use a trusted CA bundle instead of bypassing verification.",
});
}
for (const varName of RISKY_PRESENCE_VARS) {
if (env[varName] != null && env[varName] !== "") {
recordEnvironmentRead(varName, { source: envSource });
const messages = {
NODE_PATH:
"NODE_PATH is set and may cause unexpected modules to be loaded, enabling module-resolution poisoning.",
NODE_NO_WARNINGS:
"NODE_NO_WARNINGS suppresses Node.js deprecation and security warnings, which may hide exploitable conditions.",
NODE_PENDING_DEPRECATION:
"NODE_PENDING_DEPRECATION may alter runtime behavior in ways that affect cdxgen's dependency resolution.",
UV_THREADPOOL_SIZE:
"UV_THREADPOOL_SIZE alters the libuv thread pool and may affect performance or mask resource-exhaustion attacks.",
};
findings.push({
type: "environment-variable",
variable: varName,
severity: varName === "NODE_PATH" ? "high" : "medium",
message:
messages[varName] ||
`${varName} is set and may affect module resolution or runtime behavior.`,
mitigation: `Unset ${varName} before processing untrusted repositories.`,
});
}
}
// NODE_OPTIONS / CDXGEN_NODE_OPTIONS code-execution flags
if (nodeOptions) {
for (const pattern of CODE_EXECUTION_PATTERNS) {
if (pattern.test(nodeOptions)) {
findings.push({
type: "code-execution",
variable: "NODE_OPTIONS",
severity: "high",
message: `NODE_OPTIONS contains a code-execution flag matching '${pattern.source}'. Malicious code in the scanned repository may exploit this to run arbitrary commands.`,
mitigation: hasPermission
? "Remove the flag or tighten --allow-* scopes; code-execution flags can bypass permission-model boundaries."
: "Remove the flag before scanning untrusted repositories, or add --permission to enable the Node.js permission model.",
});
}
}
if (hasPermission && !env.CDXGEN_SECURE_MODE && !process.permission) {
findings.push({
type: "permission-misuse",
variable: "NODE_OPTIONS",
severity: "medium",
message:
"Permission flags are present in NODE_OPTIONS but the Node.js permission model is not active. The flags have no protective effect.",
mitigation:
"Run cdxgen with Node.js ≥20 and pass --permission on the command line, or remove the redundant flags.",
});
}
}
// JVM option injection
for (const jvmVar of [
"MVN_ARGS",
"GRADLE_ARGS",
"BAZEL_ARGS",
"JAVA_TOOL_OPTIONS",
"JDK_JAVA_OPTIONS",
]) {
const jvmOptions = env[jvmVar] || "";
recordEnvironmentRead(jvmVar, { source: envSource });
if (jvmOptions) {
for (const pattern of JVM_CODE_EXECUTION_PATTERNS) {
if (pattern.test(jvmOptions)) {
findings.push({
type: "code-execution",
variable: jvmVar,
severity: "high",
message: `${jvmVar} contains a JVM agent or module-bypass flag matching '${pattern.source}'. This may allow code injection into Java-based build tools invoked during SBOM generation.`,
mitigation: `Unset or sanitize ${jvmVar} before scanning Java/Kotlin/Scala projects.`,
});
}
}
}
}
// Proxy interception — informational
const activeProxy = PROXY_VARS.find((v) => env[v] != null && env[v] !== "");
if (activeProxy) {
recordEnvironmentRead(activeProxy, { source: envSource });
findings.push({
type: "network-interception",
variable: activeProxy,
severity: "low",
message: `An outbound proxy is configured via ${activeProxy}. Registry lookups, dependency downloads, and SBOM uploads will be routed through this proxy.`,
mitigation:
"Verify the proxy is trusted and uses TLS. Remove the variable if not required for this scan.",
});
}
// Credential exposure — detect any env var whose name follows a credential-naming convention.
for (const [varName, varValue] of Object.entries(env)) {
if (varValue && CREDENTIAL_VAR_PATTERN.test(varName)) {
recordEnvironmentRead(varName, { source: envSource });
findings.push({
type: "credential-exposure",
variable: varName,
severity: "low",
message: `${varName} matches a credential naming pattern and is set in the environment. Build tools or install scripts invoked during SBOM generation may read environment variables.`,
mitigation: `Unset ${varName} when scanning untrusted repositories. Prefer ephemeral, scoped CI credentials injected at the workflow step rather than inherited shell variables.`,
});
}
}
// Running as root — skip inside official cdxgen container images, which run as root by design.
if (
typeof process.getuid === "function" &&
process.getuid() === 0 &&
env?.CDXGEN_IN_CONTAINER !== "true"
) {
findings.push({
type: "privilege",
variable: "UID",
severity: "high",
message:
"cdxgen is running as root (UID 0). Any code executed during SBOM generation—including package manager install hooks—will run with full system privileges.",
mitigation:
"Run cdxgen as a non-privileged user. Use a container or VM with a dedicated low-privilege account.",
});
}
// Debug mode leaks internal details
if (
["debug", "verbose"].includes(env.CDXGEN_DEBUG_MODE) ||
env.SCAN_DEBUG_MODE === "debug"
) {
findings.push({
type: "debug-exposure",
variable: "CDXGEN_DEBUG_MODE",
severity: "low",
message:
"Debug/verbose logging is enabled. Sensitive values such as API tokens, file paths, and build-tool output may appear in terminal output or log files.",
mitigation:
"Disable CDXGEN_DEBUG_MODE in production and ensure debug log files are not committed or shared.",
});
}
// Deno-specific checks
// DENO_CERT installs a custom TLS CA; combined with an outbound proxy this enables MITM attacks
// on SBOM uploads and registry lookups.
if (
env.DENO_CERT !== undefined &&
env.DENO_CERT !== null &&
env.DENO_CERT !== ""
) {
findings.push({
type: "environment-variable",
variable: "DENO_CERT",
severity: "high",
message:
"DENO_CERT is set to a custom TLS certificate authority. A custom CA combined with an outbound proxy can enable man-in-the-middle attacks on registry lookups and SBOM uploads.",
mitigation:
"Unset DENO_CERT unless you explicitly require a private CA bundle. Prefer the system trust store.",
});
}
// Deno live permission model: check whether shell execution is broadly granted.
// cdxgen legitimately needs --allow-run for specific build tools; unrestricted shell access
// (sh/bash/cmd/powershell being granted) is a strong signal that --allow-all or
// --allow-run without restrictions was used, which allows package manager hooks to execute
// arbitrary commands during SBOM generation.
if (
typeof globalThis.Deno !== "undefined" &&
typeof globalThis.Deno?.permissions?.querySync === "function"
) {
try {
const shellCmds =
globalThis.Deno.build?.os === "windows"
? ["cmd", "powershell"]
: ["sh", "bash"];
const shellAllowed = shellCmds.some(
(cmd) =>
globalThis.Deno.permissions.querySync({ name: "run", command: cmd })
.state === "granted",
);
if (shellAllowed) {
findings.push({
type: "permission-misuse",
variable: "DENO_PERMISSIONS",
severity: "high",
message:
"cdxgen is running under Deno with unrestricted shell execution (--allow-all or --allow-run without restrictions). Package manager scripts invoked during SBOM generation can execute arbitrary commands.",
mitigation:
"Replace --allow-all with granular --allow-run=<tool> flags. Only allow the specific build tools required for this scan.",
});
}
} catch {
// Deno.permissions.querySync may throw in restricted or future Deno environments; ignore silently.
}
}
return findings;
}