Skip to content

[WIP] CycloneDX v2.0 Specification - #652

Draft
stevespringett wants to merge 278 commits into
masterfrom
2.0-dev
Draft

[WIP] CycloneDX v2.0 Specification#652
stevespringett wants to merge 278 commits into
masterfrom
2.0-dev

Conversation

@stevespringett

@stevespringett stevespringett commented Jun 15, 2025

Copy link
Copy Markdown
Member

Important

WORK IN PROGRESS
see Milestone for progress: https://github.com/CycloneDX/specification/milestone/2


BREAKING Changes

  • Drop schema for XML.
    To be explained further.
  • Drop schema for Protocol Buffers
    Reasoning: Downstream spec users may build ontop of JSON schema.
    To be explained further.

... TBC ...

Added

... TBD ...

Chaned

... TBD ...

Removed

... TBD ...

Misc

... TBD ...


@stevespringett stevespringett added this to the 2.0 milestone Jun 15, 2025
@stevespringett stevespringett self-assigned this Jun 15, 2025
@stevespringett stevespringett added the CDX 2.0 related to release v2.0 label Jun 15, 2025
@stevespringett stevespringett linked an issue Jun 15, 2025 that may be closed by this pull request
@jkowalleck jkowalleck changed the title CycloneDX v2.0 Specification [WIP] CycloneDX v2.0 Specification Jun 16, 2025
Comment thread .github/workflows/bundle-schema.yml Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
// Pattern for markdown links at the end
const markdownLinkPattern = /\]\([^)]+\)$/;

return urlPattern.test(text) || markdownLinkPattern.test(text);

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with 'http://' and with many repetitions of 'http://'.
// Pattern for markdown links at the end
const markdownLinkPattern = /\]\([^)]+\)$/;

return urlPattern.test(text) || markdownLinkPattern.test(text);

Check failure

Code scanning / CodeQL

Polynomial regular expression used on uncontrolled data High

This
regular expression
that depends on
library input
may run slow on strings starting with '](' and with many repetitions of ']('.
const absoluteRootPath = path.resolve(rootSchemaPath);

// Verify paths exist
await fs.access(absoluteModelsDir);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

To fix this safely without changing intended bundling behavior, validate both CLI paths against a strict policy before any filesystem access. For this script, the least disruptive policy is: only allow paths that resolve under the current working directory (process.cwd()), after normalization.

Best implementation in tools/src/main/js/bundler/bundle-schemas.js:

  • Add a helper that:
    • resolves the user path with path.resolve,
    • computes path.relative(baseDir, resolvedPath),
    • rejects if the result is absolute or starts with .. (meaning outside base).
  • Use this helper inside bundleSchemas for both modelsDirectory and rootSchemaPath instead of direct path.resolve(...).
  • Keep all existing behavior otherwise (same flow, same fs.access, same outputs), only adding early validation error on unsafe paths.

No external dependencies are needed; Node’s built-in path is sufficient.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,18 @@
     return typeof value === 'object' && value !== null;
 }
 
+function resolvePathWithinBase(baseDir, userProvidedPath, argName) {
+    const absoluteBaseDir = path.resolve(baseDir);
+    const resolvedPath = path.resolve(userProvidedPath);
+    const relativePath = path.relative(absoluteBaseDir, resolvedPath);
+
+    if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+        throw new Error(`Invalid ${argName}: path must be within ${absoluteBaseDir}`);
+    }
+
+    return resolvedPath;
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -179,8 +191,9 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const baseDir = process.cwd();
+        const absoluteModelsDir = resolvePathWithinBase(baseDir, modelsDirectory, 'modelsDirectory');
+        const absoluteRootPath = resolvePathWithinBase(baseDir, rootSchemaPath, 'rootSchemaPath');
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
EOF
@@ -16,6 +16,18 @@
return typeof value === 'object' && value !== null;
}

function resolvePathWithinBase(baseDir, userProvidedPath, argName) {
const absoluteBaseDir = path.resolve(baseDir);
const resolvedPath = path.resolve(userProvidedPath);
const relativePath = path.relative(absoluteBaseDir, resolvedPath);

if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
throw new Error(`Invalid ${argName}: path must be within ${absoluteBaseDir}`);
}

return resolvedPath;
}

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -179,8 +191,9 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const baseDir = process.cwd();
const absoluteModelsDir = resolvePathWithinBase(baseDir, modelsDirectory, 'modelsDirectory');
const absoluteRootPath = resolvePathWithinBase(baseDir, rootSchemaPath, 'rootSchemaPath');

// Verify paths exist
await fs.access(absoluteModelsDir);
Copilot is powered by AI and may make mistakes. Always verify output.

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

To fix this without changing intended functionality, validate that the user-provided rootSchemaPath resolves to a file within the provided modelsDirectory tree before using it in filesystem operations. The safest approach here is:

  1. Resolve both inputs to absolute paths.
  2. Compute path.relative(absoluteModelsDir, absoluteRootPath).
  3. Reject if the relative path is empty? (empty is fine if same path), absolute, or starts with .. (meaning outside base directory).
  4. Optionally keep existence checks after boundary validation.

In tools/src/main/js/bundler/bundle-schemas.js, update bundleSchemas right after lines 182–183 to enforce this containment rule and throw a clear error when violated. No new dependencies are required; use built-in path.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -182,6 +182,11 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        const relativeRootPath = path.relative(absoluteModelsDir, absoluteRootPath);
+        if (relativeRootPath.startsWith('..') || path.isAbsolute(relativeRootPath)) {
+            throw new Error(`Root schema path must be within models directory: ${absoluteModelsDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -182,6 +182,11 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

const relativeRootPath = path.relative(absoluteModelsDir, absoluteRootPath);
if (relativeRootPath.startsWith('..') || path.isAbsolute(relativeRootPath)) {
throw new Error(`Root schema path must be within models directory: ${absoluteModelsDir}`);
}

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
Copilot is powered by AI and may make mistakes. Always verify output.
console.log(`Output (minified): ${minifiedPath}\n`);

// Read all schema files in the models directory
const files = await fs.readdir(absoluteModelsDir);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

To fix this safely without changing intended behavior, validate both CLI-supplied paths (modelsDirectory and rootSchemaPath) against a trusted base directory before using them in filesystem operations. The best approach here is:

  1. Define a trusted base directory for this script execution (for example, process.cwd()).
  2. Resolve user inputs relative to that base.
  3. Enforce containment: ensure each resolved path is either exactly the base directory or starts with base + path.sep.
  4. Reject invalid/outside paths before any fs.access, fs.readdir, or file reads.

In tools/src/main/js/bundler/bundle-schemas.js, add a helper like resolvePathWithinBase(baseDir, userPath, label) near existing utility functions, and replace direct path.resolve(...) usage in bundleSchemas with this checked resolver. No new package dependency is needed; Node’s built-in path is sufficient.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,15 @@
     return typeof value === 'object' && value !== null;
 }
 
+function resolvePathWithinBase(baseDir, userPath, label) {
+    const absoluteBase = path.resolve(baseDir);
+    const absolutePath = path.resolve(absoluteBase, userPath);
+    if (absolutePath !== absoluteBase && !absolutePath.startsWith(absoluteBase + path.sep)) {
+        throw new Error(`Invalid ${label}: path must be within ${absoluteBase}`);
+    }
+    return absolutePath;
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -179,8 +188,9 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const baseDir = process.cwd();
+        const absoluteModelsDir = resolvePathWithinBase(baseDir, modelsDirectory, 'models directory');
+        const absoluteRootPath = resolvePathWithinBase(baseDir, rootSchemaPath, 'root schema path');
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
EOF
@@ -16,6 +16,15 @@
return typeof value === 'object' && value !== null;
}

function resolvePathWithinBase(baseDir, userPath, label) {
const absoluteBase = path.resolve(baseDir);
const absolutePath = path.resolve(absoluteBase, userPath);
if (absolutePath !== absoluteBase && !absolutePath.startsWith(absoluteBase + path.sep)) {
throw new Error(`Invalid ${label}: path must be within ${absoluteBase}`);
}
return absolutePath;
}

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -179,8 +188,9 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const baseDir = process.cwd();
const absoluteModelsDir = resolvePathWithinBase(baseDir, modelsDirectory, 'models directory');
const absoluteRootPath = resolvePathWithinBase(baseDir, rootSchemaPath, 'root schema path');

// Verify paths exist
await fs.access(absoluteModelsDir);
Copilot is powered by AI and may make mistakes. Always verify output.
const schemaPath = path.join(absoluteModelsDir, file);
console.log(` Reading ${file}...`);

const content = await fs.readFile(schemaPath, 'utf8');

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

The best fix is to canonicalize and constrain both user-provided paths to an approved root directory before any file operations.
For this script, the least disruptive approach is:

  1. Define a trusted root (for example, process.cwd() for CLI usage).
  2. Resolve user inputs relative to that root.
  3. Canonicalize with fs.realpath (after existence checks).
  4. Enforce containment with path.relative checks (!rel.startsWith('..') && !path.isAbsolute(rel)).
  5. Use only the validated canonical paths afterward.

This preserves existing behavior for normal relative invocations (like ./schema/...) while preventing directory traversal or arbitrary absolute path targeting outside the project working tree.

Edit region: tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas where absoluteModelsDir / absoluteRootPath are computed and checked (lines ~182–187 in the snippet). No new dependency is needed.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -179,13 +179,27 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const trustedRoot = await fs.realpath(process.cwd());
+        const candidateModelsDir = path.resolve(trustedRoot, modelsDirectory);
+        const candidateRootPath = path.resolve(trustedRoot, rootSchemaPath);
 
-        // Verify paths exist
-        await fs.access(absoluteModelsDir);
-        await fs.access(absoluteRootPath);
+        // Verify paths exist before canonicalization
+        await fs.access(candidateModelsDir);
+        await fs.access(candidateRootPath);
 
+        const absoluteModelsDir = await fs.realpath(candidateModelsDir);
+        const absoluteRootPath = await fs.realpath(candidateRootPath);
+
+        // Ensure both user-supplied paths are contained within trusted root
+        const modelsDirRelative = path.relative(trustedRoot, absoluteModelsDir);
+        const rootPathRelative = path.relative(trustedRoot, absoluteRootPath);
+        const modelsDirInRoot = modelsDirRelative && !modelsDirRelative.startsWith('..') && !path.isAbsolute(modelsDirRelative);
+        const rootPathInRoot = rootPathRelative && !rootPathRelative.startsWith('..') && !path.isAbsolute(rootPathRelative);
+
+        if (!modelsDirInRoot || !rootPathInRoot) {
+            throw new Error('Input paths must be within the current working directory');
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
EOF
@@ -179,13 +179,27 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const trustedRoot = await fs.realpath(process.cwd());
const candidateModelsDir = path.resolve(trustedRoot, modelsDirectory);
const candidateRootPath = path.resolve(trustedRoot, rootSchemaPath);

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
// Verify paths exist before canonicalization
await fs.access(candidateModelsDir);
await fs.access(candidateRootPath);

const absoluteModelsDir = await fs.realpath(candidateModelsDir);
const absoluteRootPath = await fs.realpath(candidateRootPath);

// Ensure both user-supplied paths are contained within trusted root
const modelsDirRelative = path.relative(trustedRoot, absoluteModelsDir);
const rootPathRelative = path.relative(trustedRoot, absoluteRootPath);
const modelsDirInRoot = modelsDirRelative && !modelsDirRelative.startsWith('..') && !path.isAbsolute(modelsDirRelative);
const rootPathInRoot = rootPathRelative && !rootPathRelative.startsWith('..') && !path.isAbsolute(rootPathRelative);

if (!modelsDirInRoot || !rootPathInRoot) {
throw new Error('Input paths must be within the current working directory');
}

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);

Copilot is powered by AI and may make mistakes. Always verify output.

// Read the root schema
console.log(`\nReading root schema...`);
const rootContent = await fs.readFile(absoluteRootPath, 'utf8');

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

General fix: validate user-controlled paths against an explicit trusted root before using them in filesystem operations. Normalization alone is insufficient; enforce containment (candidate must stay under safeRoot) after resolving absolute paths.

Best fix here (without changing intended functionality too much): in bundleSchemas, derive a safeRootDir from process.cwd() (or options.safeRootDir if provided), resolve both modelsDirectory and rootSchemaPath, and reject execution if either resolved path is outside the safe root. This keeps current CLI behavior for normal in-repo usage while preventing arbitrary absolute/parent traversal paths.

Changes needed in tools/src/main/js/bundler/bundle-schemas.js:

  • Add a small helper function to check whether a target path is inside a base directory.
  • In bundleSchemas (around lines 180–184), compute safeRootDir.
  • After computing absoluteModelsDir and absoluteRootPath, validate both with the helper and throw an error on violation.
  • No new dependencies required.
Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,11 @@
     return typeof value === 'object' && value !== null;
 }
 
+function isPathInside(basePath, targetPath) {
+    const relative = path.relative(basePath, targetPath);
+    return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -179,9 +184,14 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
+        const safeRootDir = path.resolve(options.safeRootDir || process.cwd());
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        if (!isPathInside(safeRootDir, absoluteModelsDir) || !isPathInside(safeRootDir, absoluteRootPath)) {
+            throw new Error(`Input paths must be within safe root: ${safeRootDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -16,6 +16,11 @@
return typeof value === 'object' && value !== null;
}

function isPathInside(basePath, targetPath) {
const relative = path.relative(basePath, targetPath);
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
}

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -179,9 +184,14 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const safeRootDir = path.resolve(options.safeRootDir || process.cwd());
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

if (!isPathInside(safeRootDir, absoluteModelsDir) || !isPathInside(safeRootDir, absoluteRootPath)) {
throw new Error(`Input paths must be within safe root: ${safeRootDir}`);
}

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
Copilot is powered by AI and may make mistakes. Always verify output.
// Write bundled (pretty) version
console.log('\nWriting bundled schema...');
const prettyJson = JSON.stringify(finalSchema, null, 2);
await fs.writeFile(bundledPath, prettyJson);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

To fix this without changing intended functionality, validate that the computed output paths stay within a safe, expected root directory before writing files. Since outputs are derived from rootSchemaPath, the natural safe root is absoluteModelsDir (the provided models directory). After resolving/normalizing paths, ensure:

  • absoluteRootPath is inside absoluteModelsDir
  • bundledPath and minifiedPath are inside absoluteModelsDir

Use path.relative(root, candidate) and reject when result is absolute or starts with .. (or equals ..). This is a robust containment check and avoids prefix tricks. Implement a small helper in tools/src/main/js/bundler/bundle-schemas.js and call it in bundleSchemas immediately after path resolution and before writes.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,11 @@
     return typeof value === 'object' && value !== null;
 }
 
+function isPathWithinDirectory(parentDir, targetPath) {
+    const relative = path.relative(parentDir, targetPath);
+    return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -186,6 +191,10 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
+        if (!isPathWithinDirectory(absoluteModelsDir, absoluteRootPath)) {
+            throw new Error(`Root schema path must be within models directory: ${absoluteModelsDir}`);
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
@@ -200,6 +209,10 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        if (!isPathWithinDirectory(absoluteModelsDir, bundledPath) || !isPathWithinDirectory(absoluteModelsDir, minifiedPath)) {
+            throw new Error(`Output paths must be within models directory: ${absoluteModelsDir}`);
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -16,6 +16,11 @@
return typeof value === 'object' && value !== null;
}

function isPathWithinDirectory(parentDir, targetPath) {
const relative = path.relative(parentDir, targetPath);
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
}

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -186,6 +191,10 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

if (!isPathWithinDirectory(absoluteModelsDir, absoluteRootPath)) {
throw new Error(`Root schema path must be within models directory: ${absoluteModelsDir}`);
}

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);

@@ -200,6 +209,10 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

if (!isPathWithinDirectory(absoluteModelsDir, bundledPath) || !isPathWithinDirectory(absoluteModelsDir, minifiedPath)) {
throw new Error(`Output paths must be within models directory: ${absoluteModelsDir}`);
}

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);

Copilot is powered by AI and may make mistakes. Always verify output.
console.log('\nWriting bundled schema...');
const prettyJson = JSON.stringify(finalSchema, null, 2);
await fs.writeFile(bundledPath, prettyJson);
const bundledStats = await fs.stat(bundledPath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

To fix this, validate and constrain user-provided paths before they are used to derive output file locations. In general, normalize untrusted paths (path.resolve) and enforce containment within a trusted root directory. This preserves current functionality for valid inputs while preventing arbitrary filesystem access.

Best single fix here: in bundleSchemas (around lines 182–183), resolve both modelsDirectory and rootSchemaPath, then verify both are inside a trusted base (use process.cwd() for CLI-compatible behavior). Add a helper that checks targetPath is equal to the base or starts with base + path.sep after normalization. Throw an error if validation fails. This prevents rootSchemaDir (and thus bundledPath/minifiedPath) from pointing outside the allowed workspace.

Changes needed in tools/src/main/js/bundler/bundle-schemas.js:

  • Add a small helper function (no new dependency) for containment check.
  • In bundleSchemas, define allowedBaseDir = path.resolve(process.cwd()).
  • Validate absoluteModelsDir and absoluteRootPath against allowedBaseDir before file access/writes.
Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,12 @@
     return typeof value === 'object' && value !== null;
 }
 
+function isPathWithinBase(basePath, targetPath) {
+    const normalizedBase = path.resolve(basePath);
+    const normalizedTarget = path.resolve(targetPath);
+    return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + path.sep);
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -181,7 +187,15 @@
     try {
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
+        const allowedBaseDir = path.resolve(process.cwd());
 
+        if (!isPathWithinBase(allowedBaseDir, absoluteModelsDir)) {
+            throw new Error(`Models directory must be within ${allowedBaseDir}`);
+        }
+        if (!isPathWithinBase(allowedBaseDir, absoluteRootPath)) {
+            throw new Error(`Root schema path must be within ${allowedBaseDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -16,6 +16,12 @@
return typeof value === 'object' && value !== null;
}

function isPathWithinBase(basePath, targetPath) {
const normalizedBase = path.resolve(basePath);
const normalizedTarget = path.resolve(targetPath);
return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + path.sep);
}

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -181,7 +187,15 @@
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const allowedBaseDir = path.resolve(process.cwd());

if (!isPathWithinBase(allowedBaseDir, absoluteModelsDir)) {
throw new Error(`Models directory must be within ${allowedBaseDir}`);
}
if (!isPathWithinBase(allowedBaseDir, absoluteRootPath)) {
throw new Error(`Root schema path must be within ${allowedBaseDir}`);
}

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
Copilot is powered by AI and may make mistakes. Always verify output.
const lineCount = minifiedJson.split('\n').length;
console.log(` Minified JSON is on ${lineCount} line(s)`);

await fs.writeFile(minifiedPath, minifiedJson);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

To fix this without changing intended behavior, validate that all filesystem operations remain within a trusted base directory. The best fit here is to derive a safe base from the provided models directory (already a required input), normalize/resolve all relevant paths, and reject any path that escapes that base.

In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas right after computing absoluteModelsDir/absoluteRootPath, add a helper that verifies targetPath is inside baseDir using path.relative (robust across platforms). Then:

  • Validate absoluteRootPath is within absoluteModelsDir.
  • Validate generated outputs bundledPath and minifiedPath are within absoluteModelsDir.
    If validation fails, throw an error before any write. This preserves current functionality for normal valid inputs while preventing arbitrary path writes.

No new imports or dependencies are required.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -182,6 +182,15 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        function assertPathWithinBase(baseDir, targetPath, label) {
+            const relative = path.relative(baseDir, targetPath);
+            if (relative.startsWith('..') || path.isAbsolute(relative)) {
+                throw new Error(`${label} must be within models directory: ${baseDir}`);
+            }
+        }
+
+        assertPathWithinBase(absoluteModelsDir, absoluteRootPath, 'Root schema path');
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
@@ -200,6 +209,9 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        assertPathWithinBase(absoluteModelsDir, bundledPath, 'Bundled output path');
+        assertPathWithinBase(absoluteModelsDir, minifiedPath, 'Minified output path');
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -182,6 +182,15 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

function assertPathWithinBase(baseDir, targetPath, label) {
const relative = path.relative(baseDir, targetPath);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error(`${label} must be within models directory: ${baseDir}`);
}
}

assertPathWithinBase(absoluteModelsDir, absoluteRootPath, 'Root schema path');

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
@@ -200,6 +209,9 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

assertPathWithinBase(absoluteModelsDir, bundledPath, 'Bundled output path');
assertPathWithinBase(absoluteModelsDir, minifiedPath, 'Minified output path');

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);

Copilot is powered by AI and may make mistakes. Always verify output.
console.log(` Minified JSON is on ${lineCount} line(s)`);

await fs.writeFile(minifiedPath, minifiedJson);
const minifiedStats = await fs.stat(minifiedPath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 17 hours ago

General fix: enforce that all filesystem operations using user-influenced paths are confined to an approved root directory. Normalize with path.resolve, then verify resulting paths stay inside that root.

Best fix here (without changing core functionality): in bundleSchemas, define a trusted base directory (current working directory), then reject modelsDirectory and rootSchemaPath if they resolve outside it. Additionally, verify computed output paths (bundledPath, minifiedPath) are still inside the same trusted base. This preserves existing behavior for normal in-repo usage while preventing arbitrary filesystem targets.

Changes needed in tools/src/main/js/bundler/bundle-schemas.js:

  • Add helper isPathInside(parentDir, targetPath).
  • In bundleSchemas, after resolving inputs, validate:
    • absoluteModelsDir inside trusted root
    • absoluteRootPath inside trusted root
  • After generating output paths, validate:
    • bundledPath inside trusted root
    • minifiedPath inside trusted root

No new dependencies required.

Suggested changeset 1
tools/src/main/js/bundler/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,11 @@
     return typeof value === 'object' && value !== null;
 }
 
+function isPathInside(parentDir, targetPath) {
+    const relative = path.relative(parentDir, targetPath);
+    return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -179,9 +184,18 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
+        const trustedRootDir = path.resolve(process.cwd());
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        if (!isPathInside(trustedRootDir, absoluteModelsDir)) {
+            throw new Error(`Models directory must be within project directory: ${trustedRootDir}`);
+        }
+
+        if (!isPathInside(trustedRootDir, absoluteRootPath)) {
+            throw new Error(`Root schema path must be within project directory: ${trustedRootDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
@@ -200,6 +211,10 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        if (!isPathInside(trustedRootDir, bundledPath) || !isPathInside(trustedRootDir, minifiedPath)) {
+            throw new Error(`Output paths must be within project directory: ${trustedRootDir}`);
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -16,6 +16,11 @@
return typeof value === 'object' && value !== null;
}

function isPathInside(parentDir, targetPath) {
const relative = path.relative(parentDir, targetPath);
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
}

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -179,9 +184,18 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const trustedRootDir = path.resolve(process.cwd());
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

if (!isPathInside(trustedRootDir, absoluteModelsDir)) {
throw new Error(`Models directory must be within project directory: ${trustedRootDir}`);
}

if (!isPathInside(trustedRootDir, absoluteRootPath)) {
throw new Error(`Root schema path must be within project directory: ${trustedRootDir}`);
}

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
@@ -200,6 +211,10 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

if (!isPathInside(trustedRootDir, bundledPath) || !isPathInside(trustedRootDir, minifiedPath)) {
throw new Error(`Output paths must be within project directory: ${trustedRootDir}`);
}

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);

Copilot is powered by AI and may make mistakes. Always verify output.
stevespringett and others added 9 commits November 29, 2025 17:07
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
…ment patterns.

Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
github-actions Bot and others added 2 commits December 1, 2025 03:49
Signed-off-by: Steve Springett <steve@springett.us>
jkowalleck and others added 22 commits August 18, 2026 11:31
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Steve Springett <steve@springett.us>
Co-authored-by: Jan Kowalleck <jan.kowalleck@owasp.org>
Signed-off-by: Steve Springett <steve@springett.us>
Co-authored-by: Jan Kowalleck <jan.kowalleck@owasp.org>
Signed-off-by: Steve Springett <steve@springett.us>


Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
@jkowalleck
jkowalleck marked this pull request as ready for review August 24, 2026 15:03
@jkowalleck
jkowalleck requested a review from a team as a code owner August 24, 2026 15:03
@jkowalleck
jkowalleck marked this pull request as draft August 24, 2026 15:04
jkowalleck and others added 5 commits August 24, 2026 17:06
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
…ts (#1042)

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-changes CDX 2.0 related to release v2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CycloneDX 2.0

8 participants