Skip to content

[WIP] CycloneDX v2.0 Specification - #652

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

[WIP] CycloneDX v2.0 Specification#652
stevespringett wants to merge 343 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 3 days ago

General fix: constrain user-provided paths to an approved root, after normalization, and reject inputs outside that root. For this script, the least disruptive fix is to define a trusted base directory (current working directory), resolve both modelsDirectory and rootSchemaPath against it, then verify both resolved paths are inside that base before any filesystem access.

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

  • In bundleSchemas(...), after resolving paths, add containment checks using path.relative.
  • Reject absolute user inputs early (optional but helpful) and reject any resolved path that escapes the base (.. prefix or absolute relative result).
  • Keep behavior otherwise unchanged (same arguments, same outputs), but fail fast with a clear error on unsafe paths.

No new dependencies are needed; use existing path module only.

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,9 +179,19 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const trustedBaseDir = path.resolve(process.cwd());
+        const absoluteModelsDir = path.resolve(trustedBaseDir, modelsDirectory);
+        const absoluteRootPath = path.resolve(trustedBaseDir, rootSchemaPath);
 
+        const modelsRelativeToBase = path.relative(trustedBaseDir, absoluteModelsDir);
+        const rootRelativeToBase = path.relative(trustedBaseDir, absoluteRootPath);
+        const modelsOutsideBase = modelsRelativeToBase.startsWith('..') || path.isAbsolute(modelsRelativeToBase);
+        const rootOutsideBase = rootRelativeToBase.startsWith('..') || path.isAbsolute(rootRelativeToBase);
+
+        if (modelsOutsideBase || rootOutsideBase) {
+            throw new Error('Input paths must be within the current working directory.');
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -179,9 +179,19 @@

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

const modelsRelativeToBase = path.relative(trustedBaseDir, absoluteModelsDir);
const rootRelativeToBase = path.relative(trustedBaseDir, absoluteRootPath);
const modelsOutsideBase = modelsRelativeToBase.startsWith('..') || path.isAbsolute(modelsRelativeToBase);
const rootOutsideBase = rootRelativeToBase.startsWith('..') || path.isAbsolute(rootRelativeToBase);

if (modelsOutsideBase || rootOutsideBase) {
throw new Error('Input paths must be within the current working directory.');
}

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

Use a safe-root confinement check for CLI-provided paths before filesystem operations.
Best approach here: resolve a trusted base directory (the current working directory), resolve both user inputs relative to it, and reject paths that escape that base. This preserves existing behavior for normal relative inputs while blocking traversal/out-of-scope absolute paths.

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

  1. Add a helper isPathWithinBase(baseDir, targetPath) near utility functions.
  2. In bundleSchemas, resolve safeBaseDir = path.resolve(process.cwd()).
  3. Resolve both modelsDirectory and rootSchemaPath against safeBaseDir.
  4. Validate both resolved paths are within safeBaseDir; throw an error if not.
  5. Keep existing fs.access checks and downstream behavior unchanged.

No extra package is required; Node path is already imported.

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 isPathWithinBase(baseDir, targetPath) {
+    const relative = path.relative(baseDir, targetPath);
+    return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative) || targetPath === baseDir;
+}
+
 /**
  * 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 absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const safeBaseDir = path.resolve(process.cwd());
+        const absoluteModelsDir = path.resolve(safeBaseDir, modelsDirectory);
+        const absoluteRootPath = path.resolve(safeBaseDir, rootSchemaPath);
 
+        if (!isPathWithinBase(safeBaseDir, absoluteModelsDir) || !isPathWithinBase(safeBaseDir, absoluteRootPath)) {
+            throw new Error(`Input paths must be within the working directory: ${safeBaseDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -16,6 +16,11 @@
return typeof value === 'object' && value !== null;
}

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

/**
* 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 absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const safeBaseDir = path.resolve(process.cwd());
const absoluteModelsDir = path.resolve(safeBaseDir, modelsDirectory);
const absoluteRootPath = path.resolve(safeBaseDir, rootSchemaPath);

if (!isPathWithinBase(safeBaseDir, absoluteModelsDir) || !isPathWithinBase(safeBaseDir, absoluteRootPath)) {
throw new Error(`Input paths must be within the working directory: ${safeBaseDir}`);
}

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

The best fix is to validate and constrain user-provided paths to an expected safe root before using them in filesystem operations. Here, keep existing functionality but require both modelsDirectory and rootSchemaPath to remain inside the current working directory tree (project context), after canonicalization.

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

  1. Add a helper that:
    • resolves the candidate path against a trusted base (process.cwd()),
    • canonicalizes with fs.realpath (to handle symlinks),
    • verifies the result is under the trusted base using path.relative checks.
  2. In bundleSchemas, replace direct path.resolve(...) assignments with calls to this helper for both inputs.
  3. Keep existing behavior otherwise (still checks existence, still generates outputs in root schema dir).

This addresses the flagged sink at line 207 because absoluteModelsDir is no longer uncontrolled; it is normalized and containment-checked.

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
@@ -3,6 +3,19 @@
 const fs = require('fs').promises;
 const path = require('path');
 
+async function resolvePathWithin(baseDir, userProvidedPath, argumentName) {
+    const absoluteBase = await fs.realpath(path.resolve(baseDir));
+    const resolvedCandidate = path.resolve(absoluteBase, userProvidedPath);
+    const absoluteCandidate = await fs.realpath(resolvedCandidate);
+    const relative = path.relative(absoluteBase, absoluteCandidate);
+
+    if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
+        throw new Error(`Invalid ${argumentName}: path must be within ${absoluteBase}`);
+    }
+
+    return absoluteCandidate;
+}
+
 // Default list of external schema files to bypass for validation and rewriting.
 // This constant is used as the default value for ref exceptions; can be overridden via options.refExceptions.
 const DEFAULT_REF_EXCEPTION_FILES = [
@@ -179,8 +192,9 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const safeBaseDir = process.cwd();
+        const absoluteModelsDir = await resolvePathWithin(safeBaseDir, modelsDirectory, 'modelsDirectory');
+        const absoluteRootPath = await resolvePathWithin(safeBaseDir, rootSchemaPath, 'rootSchemaPath');
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
EOF
@@ -3,6 +3,19 @@
const fs = require('fs').promises;
const path = require('path');

async function resolvePathWithin(baseDir, userProvidedPath, argumentName) {
const absoluteBase = await fs.realpath(path.resolve(baseDir));
const resolvedCandidate = path.resolve(absoluteBase, userProvidedPath);
const absoluteCandidate = await fs.realpath(resolvedCandidate);
const relative = path.relative(absoluteBase, absoluteCandidate);

if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
throw new Error(`Invalid ${argumentName}: path must be within ${absoluteBase}`);
}

return absoluteCandidate;
}

// Default list of external schema files to bypass for validation and rewriting.
// This constant is used as the default value for ref exceptions; can be overridden via options.refExceptions.
const DEFAULT_REF_EXCEPTION_FILES = [
@@ -179,8 +192,9 @@

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

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

To fix this safely without changing intended functionality, validate and constrain all file reads to a canonical trusted base directory derived from the provided modelsDirectory, and verify each resolved file path stays within that base before reading.

Best approach in this file:

  1. Canonicalize modelsDirectory once using fs.realpath(...) after existence check.
  2. For every file from readdir, resolve and canonicalize the file path (path.resolve + fs.realpath).
  3. Enforce containment: ensure canonical file path is inside canonical models directory using path.relative(...) checks.
  4. Skip or reject entries that escape the directory (including symlink traversal).
  5. Use the validated canonical path in fs.readFile.

This keeps behavior the same for valid schema directories, but blocks path traversal/symlink escape scenarios and satisfies the CodeQL concern.

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
@@ -186,10 +186,13 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
+        // Canonicalize models directory to prevent traversal/symlink escape
+        const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
-        console.log(`Models directory: ${absoluteModelsDir}`);
+        console.log(`Models directory: ${canonicalModelsDir}`);
         console.log(`Root schema: ${absoluteRootPath}`);
 
         // Generate output filenames
@@ -204,7 +204,7 @@
         console.log(`Output (minified): ${minifiedPath}\n`);
 
         // Read all schema files in the models directory
-        const files = await fs.readdir(absoluteModelsDir);
+        const files = await fs.readdir(canonicalModelsDir);
         const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));
 
         console.log(`Found ${schemaFiles.length} schema files in models directory`);
@@ -214,10 +214,16 @@
         let detectedSchemaVersion = null;
 
         for (const file of schemaFiles) {
-            const schemaPath = path.join(absoluteModelsDir, file);
+            const schemaPath = path.resolve(canonicalModelsDir, file);
+            const canonicalSchemaPath = await fs.realpath(schemaPath);
+            const relativeSchemaPath = path.relative(canonicalModelsDir, canonicalSchemaPath);
+            if (relativeSchemaPath.startsWith('..') || path.isAbsolute(relativeSchemaPath)) {
+                throw new Error(`Schema path escapes models directory: ${file}`);
+            }
+
             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
@@ -225,7 +229,7 @@
                 detectedSchemaVersion = schema.$schema;
             }
 
-            schemas[schemaPath] = schema;
+            schemas[canonicalSchemaPath] = schema;
         }
 
         // Read the root schema
EOF
@@ -186,10 +186,13 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

// Canonicalize models directory to prevent traversal/symlink escape
const canonicalModelsDir = await fs.realpath(absoluteModelsDir);

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

console.log(`Models directory: ${absoluteModelsDir}`);
console.log(`Models directory: ${canonicalModelsDir}`);
console.log(`Root schema: ${absoluteRootPath}`);

// Generate output filenames
@@ -204,7 +204,7 @@
console.log(`Output (minified): ${minifiedPath}\n`);

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

console.log(`Found ${schemaFiles.length} schema files in models directory`);
@@ -214,10 +214,16 @@
let detectedSchemaVersion = null;

for (const file of schemaFiles) {
const schemaPath = path.join(absoluteModelsDir, file);
const schemaPath = path.resolve(canonicalModelsDir, file);
const canonicalSchemaPath = await fs.realpath(schemaPath);
const relativeSchemaPath = path.relative(canonicalModelsDir, canonicalSchemaPath);
if (relativeSchemaPath.startsWith('..') || path.isAbsolute(relativeSchemaPath)) {
throw new Error(`Schema path escapes models directory: ${file}`);
}

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
@@ -225,7 +229,7 @@
detectedSchemaVersion = schema.$schema;
}

schemas[schemaPath] = schema;
schemas[canonicalSchemaPath] = schema;
}

// Read the root schema
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 3 days ago

To fix this without changing intended functionality, validate and constrain both CLI-supplied paths (modelsDirectory, rootSchemaPath) to a known safe root (the current working directory is a practical default for this script). Use path.resolve to normalize, then enforce that resolved targets are within the resolved safe root using path.relative (safer than naive startsWith, especially across platforms). Reject paths that resolve outside the safe root or to different drive roots (Windows).

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

  1. Add a helper isPathInside(parent, child) near utility functions.
  2. In bundleSchemas, define safeRoot = path.resolve(process.cwd()).
  3. Resolve user inputs relative to safeRoot and validate both with isPathInside.
  4. Throw an error before any filesystem access if validation fails.

No new 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
@@ -16,6 +16,11 @@
     return typeof value === 'object' && value !== null;
 }
 
+function isPathInside(parentPath, childPath) {
+    const relative = path.relative(parentPath, childPath);
+    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 absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const safeRoot = path.resolve(process.cwd());
+        const absoluteModelsDir = path.resolve(safeRoot, modelsDirectory);
+        const absoluteRootPath = path.resolve(safeRoot, rootSchemaPath);
 
+        if (!isPathInside(safeRoot, absoluteModelsDir) || !isPathInside(safeRoot, absoluteRootPath)) {
+            throw new Error(`Input paths must be within working directory: ${safeRoot}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -16,6 +16,11 @@
return typeof value === 'object' && value !== null;
}

function isPathInside(parentPath, childPath) {
const relative = path.relative(parentPath, childPath);
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 absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const safeRoot = path.resolve(process.cwd());
const absoluteModelsDir = path.resolve(safeRoot, modelsDirectory);
const absoluteRootPath = path.resolve(safeRoot, rootSchemaPath);

if (!isPathInside(safeRoot, absoluteModelsDir) || !isPathInside(safeRoot, absoluteRootPath)) {
throw new Error(`Input paths must be within working directory: ${safeRoot}`);
}

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

The safest fix without changing intended functionality is to constrain file writes to a trusted base directory, and reject any resolved output path outside that base.
In this file, a practical trusted base is the provided modelsDirectory (already an input and expected project scope). We should:

  1. Resolve and canonicalize (fs.realpath) modelsDirectory early.
  2. Resolve candidate output paths from rootSchemaDir.
  3. Canonicalize the output directory and verify it is inside the canonical models directory using a robust prefix check (relative + absolute/path traversal checks).
  4. Fail fast with an error if outside boundary.
  5. Use the validated output paths for writeFile/stat.

Changes are confined to tools/src/main/js/bundler/bundle-schemas.js inside bundleSchemas near lines 182–201 and do not require external dependencies.

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
@@ -186,9 +186,16 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
+        const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
+        const canonicalRootSchemaDir = await fs.realpath(rootSchemaDir);
 
+        const relativeOutputDir = path.relative(canonicalModelsDir, canonicalRootSchemaDir);
+        if (relativeOutputDir.startsWith('..') || path.isAbsolute(relativeOutputDir)) {
+            throw new Error(`Refusing to write output outside models directory: ${canonicalRootSchemaDir}`);
+        }
+
         console.log(`Models directory: ${absoluteModelsDir}`);
         console.log(`Root schema: ${absoluteRootPath}`);
 
@@ -197,8 +201,8 @@
         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.join(canonicalRootSchemaDir, bundledFilename);
+        const minifiedPath = path.join(canonicalRootSchemaDir, minifiedFilename);
 
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
EOF
@@ -186,9 +186,16 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
const canonicalRootSchemaDir = await fs.realpath(rootSchemaDir);

const relativeOutputDir = path.relative(canonicalModelsDir, canonicalRootSchemaDir);
if (relativeOutputDir.startsWith('..') || path.isAbsolute(relativeOutputDir)) {
throw new Error(`Refusing to write output outside models directory: ${canonicalRootSchemaDir}`);
}

console.log(`Models directory: ${absoluteModelsDir}`);
console.log(`Root schema: ${absoluteRootPath}`);

@@ -197,8 +201,8 @@
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.join(canonicalRootSchemaDir, bundledFilename);
const minifiedPath = path.join(canonicalRootSchemaDir, minifiedFilename);

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

To fix this without changing intended functionality, validate that both user-provided paths resolve under an expected safe base directory (for this script, the current working directory is a practical default), and only then proceed. This keeps legitimate usage (paths inside repo/workspace) while blocking traversal/absolute-path abuse that targets unrelated filesystem locations.

In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas right after resolving paths (absoluteModelsDir, absoluteRootPath), add canonicalization (fs.realpath) and enforce containment with path.relative(...) checks. Then use the canonical paths for subsequent operations (dirname/basename, access, outputs). This addresses CodeQL’s taint-to-sink path by introducing explicit validation before bundledPath/minifiedPath are built and written.

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
@@ -186,12 +186,26 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Validate user-provided paths stay within the workspace (current working directory)
+        const safeRootDir = await fs.realpath(process.cwd());
+        const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
+        const canonicalRootPath = await fs.realpath(absoluteRootPath);
 
-        console.log(`Models directory: ${absoluteModelsDir}`);
-        console.log(`Root schema: ${absoluteRootPath}`);
+        const modelsRelative = path.relative(safeRootDir, canonicalModelsDir);
+        const rootRelative = path.relative(safeRootDir, canonicalRootPath);
+        const modelsInsideSafeRoot = modelsRelative && !modelsRelative.startsWith('..') && !path.isAbsolute(modelsRelative);
+        const rootInsideSafeRoot = rootRelative && !rootRelative.startsWith('..') && !path.isAbsolute(rootRelative);
 
+        if (!modelsInsideSafeRoot || !rootInsideSafeRoot) {
+            throw new Error('Input paths must resolve within the current working directory.');
+        }
+
+        const rootSchemaFilename = path.basename(canonicalRootPath);
+        const rootSchemaDir = path.dirname(canonicalRootPath);
+
+        console.log(`Models directory: ${canonicalModelsDir}`);
+        console.log(`Root schema: ${canonicalRootPath}`);
+
         // Generate output filenames
         const baseFilename = makeSchemaName(rootSchemaFilename);
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
EOF
@@ -186,12 +186,26 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Validate user-provided paths stay within the workspace (current working directory)
const safeRootDir = await fs.realpath(process.cwd());
const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
const canonicalRootPath = await fs.realpath(absoluteRootPath);

console.log(`Models directory: ${absoluteModelsDir}`);
console.log(`Root schema: ${absoluteRootPath}`);
const modelsRelative = path.relative(safeRootDir, canonicalModelsDir);
const rootRelative = path.relative(safeRootDir, canonicalRootPath);
const modelsInsideSafeRoot = modelsRelative && !modelsRelative.startsWith('..') && !path.isAbsolute(modelsRelative);
const rootInsideSafeRoot = rootRelative && !rootRelative.startsWith('..') && !path.isAbsolute(rootRelative);

if (!modelsInsideSafeRoot || !rootInsideSafeRoot) {
throw new Error('Input paths must resolve within the current working directory.');
}

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

console.log(`Models directory: ${canonicalModelsDir}`);
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.
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 3 days ago

To fix this without changing core functionality, validate that the computed output paths remain inside an approved root directory before writing. The most robust approach here is:

  • Resolve and normalize a trusted root (use absoluteModelsDir since it is already validated and represents the schema workspace).
  • Build candidate output paths as before.
  • Canonicalize with path.resolve.
  • Enforce containment using path.relative checks (relative !== '', not absolute, and no .. prefix).
  • Throw an error if either output path escapes the safe root.

Apply this in tools/src/main/js/bundler/bundle-schemas.js inside bundleSchemas, immediately after generating bundledPath/minifiedPath (around lines 200–202) and before any writes. No new dependency is needed; path is already imported.

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
@@ -197,9 +197,29 @@
         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);
 
+        // Enforce that output files remain within the models directory.
+        // This prevents user-controlled CLI paths from writing outside the allowed workspace.
+        const safeOutputRoot = absoluteModelsDir;
+        const bundledRelative = path.relative(safeOutputRoot, bundledPath);
+        const minifiedRelative = path.relative(safeOutputRoot, minifiedPath);
+        const isBundledWithinRoot =
+            bundledRelative !== '' &&
+            !bundledRelative.startsWith('..') &&
+            !path.isAbsolute(bundledRelative);
+        const isMinifiedWithinRoot =
+            minifiedRelative !== '' &&
+            !minifiedRelative.startsWith('..') &&
+            !path.isAbsolute(minifiedRelative);
+
+        if (!isBundledWithinRoot || !isMinifiedWithinRoot) {
+            throw new Error(
+                `Refusing to write schema output outside models directory: ${safeOutputRoot}`
+            );
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -197,9 +197,29 @@
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);

// Enforce that output files remain within the models directory.
// This prevents user-controlled CLI paths from writing outside the allowed workspace.
const safeOutputRoot = absoluteModelsDir;
const bundledRelative = path.relative(safeOutputRoot, bundledPath);
const minifiedRelative = path.relative(safeOutputRoot, minifiedPath);
const isBundledWithinRoot =
bundledRelative !== '' &&
!bundledRelative.startsWith('..') &&
!path.isAbsolute(bundledRelative);
const isMinifiedWithinRoot =
minifiedRelative !== '' &&
!minifiedRelative.startsWith('..') &&
!path.isAbsolute(minifiedRelative);

if (!isBundledWithinRoot || !isMinifiedWithinRoot) {
throw new Error(
`Refusing to write schema output outside models directory: ${safeOutputRoot}`
);
}

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

To fix this safely without changing intended functionality, constrain all file operations to a trusted root (the provided modelsDirectory) and reject rootSchemaPath values outside that root after canonicalization.

Best approach in this file:

  1. Canonicalize both inputs with fs.realpath(...) after existence checks.
  2. Verify:
    • canonical root schema path is inside canonical models directory
    • canonical root schema directory is inside canonical models directory
  3. Build output paths from the canonical root schema directory.
  4. Optionally normalize output paths with path.resolve(...) and re-check containment before write/stat for defense in depth.

Concretely, update bundleSchemas in tools/src/main/js/bundler/bundle-schemas.js around lines 182–201 to use realpath and containment checks (path.relative(...)-based check), and fail fast with a clear error if violated.

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
@@ -186,19 +186,35 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Canonicalize paths (resolves symlinks) before containment checks
+        const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
+        const canonicalRootPath = await fs.realpath(absoluteRootPath);
 
-        console.log(`Models directory: ${absoluteModelsDir}`);
-        console.log(`Root schema: ${absoluteRootPath}`);
+        const rootSchemaFilename = path.basename(canonicalRootPath);
+        const rootSchemaDir = path.dirname(canonicalRootPath);
 
+        // Ensure root schema is inside the trusted models directory
+        const rootPathRelative = path.relative(canonicalModelsDir, canonicalRootPath);
+        if (rootPathRelative.startsWith('..') || path.isAbsolute(rootPathRelative)) {
+            throw new Error(`Root schema path must be within models directory: ${canonicalModelsDir}`);
+        }
+
+        // Ensure output directory is also inside the trusted models directory
+        const rootDirRelative = path.relative(canonicalModelsDir, rootSchemaDir);
+        if (rootDirRelative.startsWith('..') || path.isAbsolute(rootDirRelative)) {
+            throw new Error(`Root schema directory must be within models directory: ${canonicalModelsDir}`);
+        }
+
+        console.log(`Models directory: ${canonicalModelsDir}`);
+        console.log(`Root schema: ${canonicalRootPath}`);
+
         // Generate output filenames
         const baseFilename = makeSchemaName(rootSchemaFilename);
         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);
 
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
EOF
@@ -186,19 +186,35 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Canonicalize paths (resolves symlinks) before containment checks
const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
const canonicalRootPath = await fs.realpath(absoluteRootPath);

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

// Ensure root schema is inside the trusted models directory
const rootPathRelative = path.relative(canonicalModelsDir, canonicalRootPath);
if (rootPathRelative.startsWith('..') || path.isAbsolute(rootPathRelative)) {
throw new Error(`Root schema path must be within models directory: ${canonicalModelsDir}`);
}

// Ensure output directory is also inside the trusted models directory
const rootDirRelative = path.relative(canonicalModelsDir, rootSchemaDir);
if (rootDirRelative.startsWith('..') || path.isAbsolute(rootDirRelative)) {
throw new Error(`Root schema directory must be within models directory: ${canonicalModelsDir}`);
}

console.log(`Models directory: ${canonicalModelsDir}`);
console.log(`Root schema: ${canonicalRootPath}`);

// Generate output filenames
const baseFilename = makeSchemaName(rootSchemaFilename);
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);

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);
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
stevespringett and others added 14 commits January 13, 2026 18:55
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: Basil Hess <bhe@zurich.ibm.com>
…oFunctions and relatedCryptoMaterial/keyUsage

- Makes sure meta:enum descriptions are added for new definitions in the PR
- Adds riscv64/riscv32 to implementation platforms

Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
… Orders new entries alphabetically in the enum

Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
This PR extends the list of cryptography-related behaviors, as discussed
in today's Cryptography WG meeting.

The google sheet is synced with those entries.
Signed-off-by: Steve Springett <steve@springett.us>
Implement the following features for CBOM v2.0 as described in #738 

- Change implementationPlatform to array to support multiple platforms
- Add keyUsage property to cryptoProperties and
relatedCryptoMaterialProperties
(open string array with examples: CIPHER, SIGN, VERIFY, WRAP, UNWRAP,
etc.)
- Add secProperties to algorithmProperties for security properties
(open string array with examples: IND-CPA, IND-CCA, SUF-CMA, EUF-CMA,
etc.)
- Extend evidence/occurrences with system metadata: accountInfo,
systemOwner
- Extend evidence/occurrences with process metadata: startTime, endTime,
usageCount
- Change securedBy.algorithmRef to array of refs to support linking
multiple
  securing assets (algorithms, hardware, keys, etc.)

Fixes #738

Adds support for pss in cryptoProperties.algorithmProperties.padding
Fixes #747

Adds support for key agreement or exchange in
cryptoProperties.algorithmProperties.cryptoFunctions
Fixes #748 

Adds support for additional cipher modes in
cryptoProperties.algorithmProperties.mode
Fixes #749
Signed-off-by: Steve Springett <steve@springett.us>
jkowalleck and others added 30 commits August 28, 2026 11:50
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: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Co-authored-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>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
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

7 participants