Skip to content

[WIP] CycloneDX v2.0 Specification - #652

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

stevespringett wants to merge 384 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 ']('.
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed

// 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 6 days ago

To fix this safely without changing intended functionality, validate that the provided rootSchemaPath is contained within the provided modulesDirectory (or within an expected safe root), after canonicalization. The most robust approach here is:

  1. Resolve both input paths to absolute paths (path.resolve) — already done.
  2. Canonicalize them with fs.realpath to collapse symlinks.
  3. Enforce containment (root must be equal to or under modules directory) before any further filesystem use.
  4. Fail fast with a clear error if validation fails.

This should be implemented in tools/src/main/js/bundler/bundle-schemas.js inside bundleSchemas, right after initial resolve/access and before using the root schema path further. Also slightly adjust isInsideDir so it treats the directory itself as “inside” (needed for robust boundary checks).

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
@@ -12,7 +12,7 @@
  */
 function isInsideDir(filePath, dirPath) {
     const rel = path.relative(dirPath, filePath);
-    return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
+    return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
 }
 
 /**
@@ -200,12 +200,19 @@
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Canonicalize to prevent traversal via symlinks and enforce trust boundary.
+        const canonicalModulesDir = await fs.realpath(absoluteModulesDir);
+        const canonicalRootPath = await fs.realpath(absoluteRootPath);
+        if (!isInsideDir(canonicalRootPath, canonicalModulesDir)) {
+            throw new Error(`Root schema path must be inside modules directory: ${canonicalModulesDir}`);
+        }
 
-        console.log(`Modules directory: ${absoluteModulesDir}`);
-        console.log(`Root schema: ${absoluteRootPath}`);
+        const rootSchemaFilename = path.basename(canonicalRootPath);
+        const rootSchemaDir = path.dirname(canonicalRootPath);
 
+        console.log(`Modules directory: ${canonicalModulesDir}`);
+        console.log(`Root schema: ${canonicalRootPath}`);
+
         // Generate output filenames
         const baseFilename = makeSchemaName(rootSchemaFilename);
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
EOF
@@ -12,7 +12,7 @@
*/
function isInsideDir(filePath, dirPath) {
const rel = path.relative(dirPath, filePath);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}

/**
@@ -200,12 +200,19 @@
await fs.access(absoluteModulesDir);
await fs.access(absoluteRootPath);

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Canonicalize to prevent traversal via symlinks and enforce trust boundary.
const canonicalModulesDir = await fs.realpath(absoluteModulesDir);
const canonicalRootPath = await fs.realpath(absoluteRootPath);
if (!isInsideDir(canonicalRootPath, canonicalModulesDir)) {
throw new Error(`Root schema path must be inside modules directory: ${canonicalModulesDir}`);
}

console.log(`Modules directory: ${absoluteModulesDir}`);
console.log(`Root schema: ${absoluteRootPath}`);
const rootSchemaFilename = path.basename(canonicalRootPath);
const rootSchemaDir = path.dirname(canonicalRootPath);

console.log(`Modules directory: ${canonicalModulesDir}`);
console.log(`Root schema: ${canonicalRootPath}`);

// Generate output filenames
const baseFilename = makeSchemaName(rootSchemaFilename);
const bundledFilename = `${baseFilename}-bundled.schema.json`;
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread tools/src/main/js/bundler/bundle-schemas.js Fixed
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 6 days ago

To fix this safely without changing intended behavior, validate and constrain all filesystem access to an allowed root directory after canonicalization (resolve symlinks), then ensure both:

  1. modulesDirectory is under that safe root, and
  2. every computed schemaPath is still inside the validated modules directory.

In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas:

  • Canonicalize modulesDirectory and rootSchemaPath with fs.realpath(...) after fs.access(...).
  • Derive a safe boundary directory (best fit here: current working directory via process.cwd() canonicalized).
  • Reject paths not inside that boundary using existing isInsideDir(...) (extended to allow equality for directory checks).
  • In the loop, canonicalize each schemaPath before reading and verify it is inside the canonical modules directory to prevent symlink escape.
    No new dependencies are 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
@@ -10,9 +10,9 @@
 /**
  * Whether `filePath` lives (at any depth) inside `dirPath`. Both must be absolute.
  */
-function isInsideDir(filePath, dirPath) {
+function isInsideDir(filePath, dirPath, allowEqual = false) {
     const rel = path.relative(dirPath, filePath);
-    return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
+    return (allowEqual && rel === '') || (rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel));
 }
 
 /**
@@ -200,9 +199,21 @@
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Canonicalize user-provided paths (resolves symlinks) and enforce they stay under a safe root.
+        const safeRootDir = await fs.realpath(process.cwd());
+        const canonicalModulesDir = await fs.realpath(absoluteModulesDir);
+        const canonicalRootPath = await fs.realpath(absoluteRootPath);
 
+        if (!isInsideDir(canonicalModulesDir, safeRootDir, true)) {
+            throw new Error(`Modules directory must be within working directory: ${safeRootDir}`);
+        }
+        if (!isInsideDir(canonicalRootPath, safeRootDir, true)) {
+            throw new Error(`Root schema path must be within working directory: ${safeRootDir}`);
+        }
+
+        const rootSchemaFilename = path.basename(canonicalRootPath);
+        const rootSchemaDir = path.dirname(canonicalRootPath);
+
         console.log(`Modules directory: ${absoluteModulesDir}`);
         console.log(`Root schema: ${absoluteRootPath}`);
 
@@ -218,7 +228,7 @@
         console.log(`Output (minified): ${minifiedPath}\n`);
 
         // Read all schema files in the modules directory
-        const files = await fs.readdir(absoluteModulesDir);
+        const files = await fs.readdir(canonicalModulesDir);
         const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));
 
         console.log(`Found ${schemaFiles.length} schema files in modules directory`);
@@ -228,10 +238,14 @@
         let detectedSchemaVersion = null;
 
         for (const file of schemaFiles) {
-            const schemaPath = path.join(absoluteModulesDir, file);
+            const schemaPath = path.join(canonicalModulesDir, file);
+            const canonicalSchemaPath = await fs.realpath(schemaPath);
+            if (!isInsideDir(canonicalSchemaPath, canonicalModulesDir, true)) {
+                throw new Error(`Schema file resolves outside modules directory: ${schemaPath}`);
+            }
             console.log(`  Reading ${file}...`);
 
-            const content = await fs.readFile(schemaPath, 'utf8');
+            const content = await fs.readFile(canonicalSchemaPath, 'utf8');
             const schema = JSON.parse(content);
 
             // Detect the $schema version from the first schema that has it
EOF
@@ -10,9 +10,9 @@
/**
* Whether `filePath` lives (at any depth) inside `dirPath`. Both must be absolute.
*/
function isInsideDir(filePath, dirPath) {
function isInsideDir(filePath, dirPath, allowEqual = false) {
const rel = path.relative(dirPath, filePath);
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
return (allowEqual && rel === '') || (rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel));
}

/**
@@ -200,9 +199,21 @@
await fs.access(absoluteModulesDir);
await fs.access(absoluteRootPath);

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Canonicalize user-provided paths (resolves symlinks) and enforce they stay under a safe root.
const safeRootDir = await fs.realpath(process.cwd());
const canonicalModulesDir = await fs.realpath(absoluteModulesDir);
const canonicalRootPath = await fs.realpath(absoluteRootPath);

if (!isInsideDir(canonicalModulesDir, safeRootDir, true)) {
throw new Error(`Modules directory must be within working directory: ${safeRootDir}`);
}
if (!isInsideDir(canonicalRootPath, safeRootDir, true)) {
throw new Error(`Root schema path must be within working directory: ${safeRootDir}`);
}

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

console.log(`Modules directory: ${absoluteModulesDir}`);
console.log(`Root schema: ${absoluteRootPath}`);

@@ -218,7 +228,7 @@
console.log(`Output (minified): ${minifiedPath}\n`);

// Read all schema files in the modules directory
const files = await fs.readdir(absoluteModulesDir);
const files = await fs.readdir(canonicalModulesDir);
const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));

console.log(`Found ${schemaFiles.length} schema files in modules directory`);
@@ -228,10 +238,14 @@
let detectedSchemaVersion = null;

for (const file of schemaFiles) {
const schemaPath = path.join(absoluteModulesDir, file);
const schemaPath = path.join(canonicalModulesDir, file);
const canonicalSchemaPath = await fs.realpath(schemaPath);
if (!isInsideDir(canonicalSchemaPath, canonicalModulesDir, true)) {
throw new Error(`Schema file resolves outside modules directory: ${schemaPath}`);
}
console.log(` Reading ${file}...`);

const content = await fs.readFile(schemaPath, 'utf8');
const content = await fs.readFile(canonicalSchemaPath, 'utf8');
const schema = JSON.parse(content);

// Detect the $schema version from the first schema that has it
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 6 days ago

To fix this safely without changing intended bundling behavior, validate that the resolved root schema path is inside an allowed base directory before using it. The best fit in this file is to constrain absoluteRootPath to the parent directory of absoluteModulesDir (which matches the shown CLI example layout: .../schema/<ver>/modules and .../schema/<ver>/root.schema.json).

What to change (tools/src/main/js/bundler/bundle-schemas.js):

  1. In bundleSchemas, after computing absoluteModulesDir and absoluteRootPath, compute allowedRootBaseDir = path.dirname(absoluteModulesDir).
  2. Use existing isInsideDir helper (plus equality check) to enforce:
    • absoluteRootPath === allowedRootBaseDir OR inside it.
  3. Throw an error if validation fails, before any file access.
  4. Keep all existing behavior otherwise unchanged.

No new dependency is required; existing path and helper function are 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
@@ -195,7 +195,14 @@
     try {
         const absoluteModulesDir = path.resolve(modulesDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
+        const allowedRootBaseDir = path.dirname(absoluteModulesDir);
 
+        // Ensure the root schema path stays within the expected schema root directory
+        // (for example: <schema-version>/modules + <schema-version>/root.schema.json)
+        if (absoluteRootPath !== allowedRootBaseDir && !isInsideDir(absoluteRootPath, allowedRootBaseDir)) {
+            throw new Error(`Root schema path must be inside: ${allowedRootBaseDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
EOF
@@ -195,7 +195,14 @@
try {
const absoluteModulesDir = path.resolve(modulesDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const allowedRootBaseDir = path.dirname(absoluteModulesDir);

// Ensure the root schema path stays within the expected schema root directory
// (for example: <schema-version>/modules + <schema-version>/root.schema.json)
if (absoluteRootPath !== allowedRootBaseDir && !isInsideDir(absoluteRootPath, allowedRootBaseDir)) {
throw new Error(`Root schema path must be inside: ${allowedRootBaseDir}`);
}

// Verify paths exist
await fs.access(absoluteModulesDir);
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 6 days ago

To fix this without changing normal functionality, constrain all file writes to a trusted workspace root (here: modulesDirectory) and reject a rootSchemaPath that is outside that root.
Best approach in this file:

  1. Compute canonical absolute paths for both modulesDirectory and rootSchemaPath using fs.realpath after existence checks (resolves symlinks too).
  2. Enforce containment: ensure canonical root schema path is inside canonical modules directory (or equal to it if desired).
  3. Build output paths from the canonical root schema directory and re-check the final output paths are inside canonical modules directory before writing.

This requires only edits in tools/src/main/js/bundler/bundle-schemas.js inside bundleSchemas, reusing existing isInsideDir helper and existing imports (fs, 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
@@ -200,9 +200,18 @@
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Canonicalize paths to account for symlinks before containment checks
+        const canonicalModulesDir = await fs.realpath(absoluteModulesDir);
+        const canonicalRootPath = await fs.realpath(absoluteRootPath);
 
+        // Security check: root schema must be inside modules directory
+        if (canonicalRootPath !== canonicalModulesDir && !isInsideDir(canonicalRootPath, canonicalModulesDir)) {
+            throw new Error(`Root schema path must be inside modules directory. root=${canonicalRootPath}, modules=${canonicalModulesDir}`);
+        }
+
+        const rootSchemaFilename = path.basename(canonicalRootPath);
+        const rootSchemaDir = path.dirname(canonicalRootPath);
+
         console.log(`Modules directory: ${absoluteModulesDir}`);
         console.log(`Root schema: ${absoluteRootPath}`);
 
@@ -214,6 +222,10 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        if (!isInsideDir(bundledPath, canonicalModulesDir) || !isInsideDir(minifiedPath, canonicalModulesDir)) {
+            throw new Error('Refusing to write output files outside modules directory');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -200,9 +200,18 @@
await fs.access(absoluteModulesDir);
await fs.access(absoluteRootPath);

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Canonicalize paths to account for symlinks before containment checks
const canonicalModulesDir = await fs.realpath(absoluteModulesDir);
const canonicalRootPath = await fs.realpath(absoluteRootPath);

// Security check: root schema must be inside modules directory
if (canonicalRootPath !== canonicalModulesDir && !isInsideDir(canonicalRootPath, canonicalModulesDir)) {
throw new Error(`Root schema path must be inside modules directory. root=${canonicalRootPath}, modules=${canonicalModulesDir}`);
}

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

console.log(`Modules directory: ${absoluteModulesDir}`);
console.log(`Root schema: ${absoluteRootPath}`);

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

if (!isInsideDir(bundledPath, canonicalModulesDir) || !isInsideDir(minifiedPath, canonicalModulesDir)) {
throw new Error('Refusing to write output files outside modules directory');
}

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 6 days ago

To fix this without changing intended behavior, validate that:

  1. rootSchemaPath is inside modulesDirectory (or at least within a defined safe root), and
  2. generated output files (bundledPath, minifiedPath) are also inside that same safe root.

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

  • Keep existing path.resolve.
  • Add containment checks using the existing isInsideDir helper.
  • Also allow equality for directory checks (because current helper returns false when paths are equal).
  • Throw a clear error if any check fails before any file write/stat happens.

This preserves functionality for legitimate inputs while preventing traversal/out-of-scope 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
@@ -200,6 +200,11 @@
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
 
+        // Constrain user-provided paths to the modules directory
+        if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) {
+            throw new Error(`Root schema path must be inside modules directory: ${absoluteRootPath}`);
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
@@ -211,9 +216,13 @@
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
         const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;
 
-        const bundledPath = path.join(rootSchemaDir, bundledFilename);
-        const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
+        const bundledPath = path.resolve(rootSchemaDir, bundledFilename);
+        const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename);
 
+        if (!isInsideDir(bundledPath, absoluteModulesDir) || !isInsideDir(minifiedPath, absoluteModulesDir)) {
+            throw new Error('Refusing to write output files outside modules directory');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -200,6 +200,11 @@
await fs.access(absoluteModulesDir);
await fs.access(absoluteRootPath);

// Constrain user-provided paths to the modules directory
if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) {
throw new Error(`Root schema path must be inside modules directory: ${absoluteRootPath}`);
}

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

@@ -211,9 +216,13 @@
const bundledFilename = `${baseFilename}-bundled.schema.json`;
const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;

const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
const bundledPath = path.resolve(rootSchemaDir, bundledFilename);
const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename);

if (!isInsideDir(bundledPath, absoluteModulesDir) || !isInsideDir(minifiedPath, absoluteModulesDir)) {
throw new Error('Refusing to write output files outside modules directory');
}

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

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 6 days ago

Use a containment check to ensure all write targets remain under an approved base directory, and fail fast otherwise. The best minimal fix here is to enforce that rootSchemaPath is inside modulesDirectory (or at least that generated output paths are), then validate bundledPath and minifiedPath with the existing isInsideDir helper before any write.

In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas(...) after computing absolute paths and before generating/using output paths:

  1. Ensure absoluteRootPath is under absoluteModulesDir (or equal if desired).
  2. After creating bundledPath/minifiedPath, resolve them and verify they are inside absoluteModulesDir.
  3. Throw an error if checks fail.

No new dependency is needed; reuse existing path and isInsideDir.

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
@@ -200,6 +200,11 @@
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
 
+        // Enforce that root schema and generated outputs stay within the modules directory
+        if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) {
+            throw new Error(`Root schema path must be inside modules directory: ${absoluteRootPath}`);
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
@@ -211,9 +216,16 @@
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
         const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;
 
-        const bundledPath = path.join(rootSchemaDir, bundledFilename);
-        const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
+        const bundledPath = path.resolve(rootSchemaDir, bundledFilename);
+        const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename);
 
+        if (
+            (bundledPath !== absoluteModulesDir && !isInsideDir(bundledPath, absoluteModulesDir)) ||
+            (minifiedPath !== absoluteModulesDir && !isInsideDir(minifiedPath, absoluteModulesDir))
+        ) {
+            throw new Error('Refusing to write bundled schemas outside modules directory');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -200,6 +200,11 @@
await fs.access(absoluteModulesDir);
await fs.access(absoluteRootPath);

// Enforce that root schema and generated outputs stay within the modules directory
if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) {
throw new Error(`Root schema path must be inside modules directory: ${absoluteRootPath}`);
}

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

@@ -211,9 +216,16 @@
const bundledFilename = `${baseFilename}-bundled.schema.json`;
const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;

const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
const bundledPath = path.resolve(rootSchemaDir, bundledFilename);
const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename);

if (
(bundledPath !== absoluteModulesDir && !isInsideDir(bundledPath, absoluteModulesDir)) ||
(minifiedPath !== absoluteModulesDir && !isInsideDir(minifiedPath, absoluteModulesDir))
) {
throw new Error('Refusing to write bundled schemas outside modules directory');
}

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 6 days ago

General fix: constrain user-controlled paths to a trusted base directory and reject any resolved path that escapes that base. Normalization alone is insufficient; enforce containment after resolve.

Best fix here without changing intended functionality too much:

  • In bundleSchemas (around lines 196–205), derive a trusted base from modulesDirectory (already a required input), resolve rootSchemaPath relative to it, and reject if the resulting absolute path is not inside that base.
  • Keep existing behavior for generating outputs next to root schema, but now guaranteed under trusted root.
  • Use existing isInsideDir helper (already present) plus equality check for exact base match.
  • No new dependencies needed.

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

  1. Replace current absoluteRootPath = path.resolve(rootSchemaPath) with:
    • absoluteModulesDir = path.resolve(modulesDirectory);
    • absoluteRootPath = path.resolve(absoluteModulesDir, rootSchemaPath);
    • containment check: absoluteRootPath === absoluteModulesDir || isInsideDir(absoluteRootPath, absoluteModulesDir)
    • throw on failure.
  2. Leave downstream path construction unchanged.
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
@@ -194,8 +194,12 @@
 async function bundleSchemas(modulesDirectory, rootSchemaPath, options = {}) {
     try {
         const absoluteModulesDir = path.resolve(modulesDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const absoluteRootPath = path.resolve(absoluteModulesDir, rootSchemaPath);
 
+        if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) {
+            throw new Error(`Root schema path must be inside modules directory: ${rootSchemaPath}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModulesDir);
         await fs.access(absoluteRootPath);
EOF
@@ -194,8 +194,12 @@
async function bundleSchemas(modulesDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModulesDir = path.resolve(modulesDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const absoluteRootPath = path.resolve(absoluteModulesDir, rootSchemaPath);

if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) {
throw new Error(`Root schema path must be inside modules directory: ${rootSchemaPath}`);
}

// Verify paths exist
await fs.access(absoluteModulesDir);
await fs.access(absoluteRootPath);
Copilot is powered by AI and may make mistakes. Always verify output.
@stevespringett
stevespringett force-pushed the 2.0-dev branch 4 times, most recently from f63bd4a to 9a09935 Compare December 1, 2025 19:39
jkowalleck and others added 2 commits March 31, 2026 19:59
Signed-off-by: Jan Kowalleck <jan.kowalleck@owasp.org>
Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
stevespringett and others added 9 commits May 4, 2026 13:11
…the various models.

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: Basil Hess <bhe@zurich.ibm.com>
Signed-off-by: Steve Springett <steve@springett.us>
…ed schemas. Supersedes and closes #777.

Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
jkowalleck and others added 30 commits September 10, 2026 15:30
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>
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>
# Conflicts:
#	schema/2.0/model/cyclonedx-cryptography-2.0.schema.json
- End all protocolFamiliesEnum meta:enum descriptions with a full stop
- Drop the redundant "type" keyword from enums in cryptography-defs,
  in line with the const/enum refactor on 2.0-dev
- Add the missing newline at end of cryptography-defs.schema.json
- Rename the protocolProperties.type title to "Protocol Type"
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>
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>
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>
Signed-off-by: Steve Springett <steve@springett.us>
# Conflicts:
#	.github/workflows/test_1.x_php.yml

This branch has not been deployed

No deployments
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

5 participants