Skip to content

[WIP] CycloneDX v2.0 Specification - #652

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

stevespringett wants to merge 378 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 8 minutes ago

General fix: validate user-controlled path inputs against a strict policy before using them in filesystem APIs. For this script, the least disruptive and most practical policy is: only allow paths that resolve under the current working directory (project workspace), and reject anything outside it.

Best fix in this file:

  1. Add a helper that resolves a user input path against process.cwd(), then verifies the resolved path is inside that base directory (or equal to it).
  2. Use that helper for both modelsDirectory and rootSchemaPath in the CLI block before calling bundleSchemas.
  3. Keep bundleSchemas behavior unchanged; only sanitize/validate at the trust boundary (CLI parsing).
  4. Reuse existing isInsideDir helper to avoid unnecessary new dependencies.

Edits are only in tools/src/main/js/bundler/bundle-schemas.js:

  • Add resolveCliPathWithinCwd(...) helper after isInsideDir.
  • In CLI section, resolve and validate both args, handle invalid input with a clear error + process.exit(1).
  • Pass validated absolute paths to bundleSchemas.
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 @@
 }
 
 /**
+ * Resolve a CLI-provided path and ensure it stays within the current working directory.
+ */
+function resolveCliPathWithinCwd(inputPath, label) {
+    const cwd = path.resolve(process.cwd());
+    const resolved = path.resolve(cwd, inputPath);
+    if (resolved !== cwd && !isInsideDir(resolved, cwd)) {
+        throw new Error(`${label} must be within the current working directory: ${cwd}`);
+    }
+    return resolved;
+}
+
+/**
  * Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
  * Such refs always point to external schemas: they are never bundled, rewritten nor checked.
  */
@@ -432,7 +444,17 @@
         process.exit(1);
     }
 
-    bundleSchemas(modelsDirectory, rootSchemaPath, {validate: true})
+    let safeModelsDirectory;
+    let safeRootSchemaPath;
+    try {
+        safeModelsDirectory = resolveCliPathWithinCwd(modelsDirectory, 'modelsDirectory');
+        safeRootSchemaPath = resolveCliPathWithinCwd(rootSchemaPath, 'rootSchemaPath');
+    } catch (err) {
+        console.error(`Invalid path argument: ${err.message}`);
+        process.exit(1);
+    }
+
+    bundleSchemas(safeModelsDirectory, safeRootSchemaPath, {validate: true})
         .catch(err => {
             console.error(err);
             process.exit(1);
EOF
@@ -16,6 +16,18 @@
}

/**
* Resolve a CLI-provided path and ensure it stays within the current working directory.
*/
function resolveCliPathWithinCwd(inputPath, label) {
const cwd = path.resolve(process.cwd());
const resolved = path.resolve(cwd, inputPath);
if (resolved !== cwd && !isInsideDir(resolved, cwd)) {
throw new Error(`${label} must be within the current working directory: ${cwd}`);
}
return resolved;
}

/**
* Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
* Such refs always point to external schemas: they are never bundled, rewritten nor checked.
*/
@@ -432,7 +444,17 @@
process.exit(1);
}

bundleSchemas(modelsDirectory, rootSchemaPath, {validate: true})
let safeModelsDirectory;
let safeRootSchemaPath;
try {
safeModelsDirectory = resolveCliPathWithinCwd(modelsDirectory, 'modelsDirectory');
safeRootSchemaPath = resolveCliPathWithinCwd(rootSchemaPath, 'rootSchemaPath');
} catch (err) {
console.error(`Invalid path argument: ${err.message}`);
process.exit(1);
}

bundleSchemas(safeModelsDirectory, safeRootSchemaPath, {validate: true})
.catch(err => {
console.error(err);
process.exit(1);
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 7 minutes ago

The best fix is to validate and constrain rootSchemaPath to a safe root before using it in filesystem operations. In this file, the natural safe root is modelsDirectory (already resolved as absoluteModelsDir). Keep existing behavior as much as possible by:

  1. Resolving both paths to absolute normalized paths.
  2. Verifying absoluteRootPath is equal to or inside absoluteModelsDir.
  3. Failing fast with a clear error if it is outside.

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

  • Add a helper that allows “inside-or-equal” directory checks (current isInsideDir rejects equality).
  • In bundleSchemas, right after resolving absoluteModelsDir/absoluteRootPath and before fs.access, enforce that root schema is contained within models directory.
  • Throw an error when invalid. This preserves core functionality (bundling schemas from provided model directory) while preventing path traversal / arbitrary file access via CLI input.
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,14 @@
 }
 
 /**
+ * Whether `filePath` is equal to `dirPath` or lives inside it. Both must be absolute.
+ */
+function isInsideOrEqualDir(filePath, dirPath) {
+    const rel = path.relative(dirPath, filePath);
+    return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
+}
+
+/**
  * Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
  * Such refs always point to external schemas: they are never bundled, rewritten nor checked.
  */
@@ -196,6 +204,13 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        // Ensure the root schema path is constrained to the models directory.
+        if (!isInsideOrEqualDir(absoluteRootPath, absoluteModelsDir)) {
+            throw new Error(
+                `Root schema path must be inside models directory. rootSchemaPath=${absoluteRootPath}, modelsDirectory=${absoluteModelsDir}`
+            );
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -16,6 +16,14 @@
}

/**
* Whether `filePath` is equal to `dirPath` or lives inside it. Both must be absolute.
*/
function isInsideOrEqualDir(filePath, dirPath) {
const rel = path.relative(dirPath, filePath);
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}

/**
* Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
* Such refs always point to external schemas: they are never bundled, rewritten nor checked.
*/
@@ -196,6 +204,13 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

// Ensure the root schema path is constrained to the models directory.
if (!isInsideOrEqualDir(absoluteRootPath, absoluteModelsDir)) {
throw new Error(
`Root schema path must be inside models directory. rootSchemaPath=${absoluteRootPath}, modelsDirectory=${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 7 minutes ago

To fix this safely without changing core functionality, validate that both CLI-provided paths stay within an expected safe base directory (the current working directory is a practical default for this script). Keep normalization via path.resolve, then enforce containment with the existing isInsideDir helper (plus equality check to allow the base dir itself). Reject paths outside the safe root before any filesystem access.

Best concrete fix in this file:

  • In bundleSchemas (around lines 196–202), after resolving absoluteModelsDir/absoluteRootPath, compute safeRootDir = path.resolve(process.cwd()).
  • Check both resolved paths are either equal to safeRootDir or inside it.
  • Throw an error if validation fails.
  • This introduces no new dependencies/imports and uses existing helper logic.
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,15 @@
     try {
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
+        const safeRootDir = path.resolve(process.cwd());
 
+        // Validate user-provided paths stay within the allowed workspace root
+        const modelsDirAllowed = absoluteModelsDir === safeRootDir || isInsideDir(absoluteModelsDir, safeRootDir);
+        const rootSchemaAllowed = absoluteRootPath === safeRootDir || isInsideDir(absoluteRootPath, safeRootDir);
+        if (!modelsDirAllowed || !rootSchemaAllowed) {
+            throw new Error(`Input paths must be within the current working directory: ${safeRootDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -195,7 +195,15 @@
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const safeRootDir = path.resolve(process.cwd());

// Validate user-provided paths stay within the allowed workspace root
const modelsDirAllowed = absoluteModelsDir === safeRootDir || isInsideDir(absoluteModelsDir, safeRootDir);
const rootSchemaAllowed = absoluteRootPath === safeRootDir || isInsideDir(absoluteRootPath, safeRootDir);
if (!modelsDirAllowed || !rootSchemaAllowed) {
throw new Error(`Input paths must be within the current working directory: ${safeRootDir}`);
}

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

To fix this without changing intended functionality, validate that both user-provided paths are inside an expected safe root (the current working directory), after normalization. Then, when constructing per-file paths from readdir, resolve and re-check each candidate path stays within the validated models directory before reading.

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

  1. Add a helper that normalizes and validates a user path against a base directory (using path.resolve + isInsideDir / equality check).
  2. In bundleSchemas, derive safeRootDir = path.resolve(process.cwd()).
  3. Replace direct path.resolve(modelsDirectory/rootSchemaPath) with validated absolute paths from the helper.
  4. In the schema loop, use path.resolve(absoluteModelsDir, file) and skip anything that escapes absoluteModelsDir (defense in depth).
  5. Keep behavior otherwise unchanged.

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
@@ -15,6 +15,14 @@
     return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
 }
 
+function resolveAndValidateInsideBase(userPath, baseDir, label) {
+    const absolutePath = path.resolve(userPath);
+    if (absolutePath !== baseDir && !isInsideDir(absolutePath, baseDir)) {
+        throw new Error(`${label} must be inside ${baseDir}: ${userPath}`);
+    }
+    return absolutePath;
+}
+
 /**
  * Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
  * Such refs always point to external schemas: they are never bundled, rewritten nor checked.
@@ -193,8 +201,9 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const safeRootDir = path.resolve(process.cwd());
+        const absoluteModelsDir = resolveAndValidateInsideBase(modelsDirectory, safeRootDir, 'modelsDirectory');
+        const absoluteRootPath = resolveAndValidateInsideBase(rootSchemaPath, safeRootDir, 'rootSchemaPath');
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
@@ -228,7 +237,11 @@
         let detectedSchemaVersion = null;
 
         for (const file of schemaFiles) {
-            const schemaPath = path.join(absoluteModelsDir, file);
+            const schemaPath = path.resolve(absoluteModelsDir, file);
+            if (schemaPath !== absoluteModelsDir && !isInsideDir(schemaPath, absoluteModelsDir)) {
+                console.warn(`  Skipping path outside models directory: ${file}`);
+                continue;
+            }
             console.log(`  Reading ${file}...`);
 
             const content = await fs.readFile(schemaPath, 'utf8');
EOF
@@ -15,6 +15,14 @@
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
}

function resolveAndValidateInsideBase(userPath, baseDir, label) {
const absolutePath = path.resolve(userPath);
if (absolutePath !== baseDir && !isInsideDir(absolutePath, baseDir)) {
throw new Error(`${label} must be inside ${baseDir}: ${userPath}`);
}
return absolutePath;
}

/**
* Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
* Such refs always point to external schemas: they are never bundled, rewritten nor checked.
@@ -193,8 +201,9 @@

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

// Verify paths exist
await fs.access(absoluteModelsDir);
@@ -228,7 +237,11 @@
let detectedSchemaVersion = null;

for (const file of schemaFiles) {
const schemaPath = path.join(absoluteModelsDir, file);
const schemaPath = path.resolve(absoluteModelsDir, file);
if (schemaPath !== absoluteModelsDir && !isInsideDir(schemaPath, absoluteModelsDir)) {
console.warn(` Skipping path outside models directory: ${file}`);
continue;
}
console.log(` Reading ${file}...`);

const content = await fs.readFile(schemaPath, 'utf8');
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 8 minutes ago

Best fix: enforce a safe root directory boundary for both modelsDirectory and rootSchemaPath before any filesystem read/list operations. General approach: canonicalize candidate paths and root path, then verify candidate is inside root; reject otherwise.

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

  1. Add a helper that resolves symlinks/canonical paths via fs.realpath and checks containment with existing isInsideDir (plus equality allowance where appropriate).
  2. In bundleSchemas, after resolving input paths and before fs.access/readFile/readdir, compute a trusted base (here: process.cwd()), canonicalize it, and validate:
    • absoluteModelsDir must be inside base dir.
    • absoluteRootPath must be inside base dir.
  3. Throw an explicit error when validation fails.

This preserves existing functionality for normal project-relative usage while blocking traversal to unexpected filesystem locations.

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
@@ -15,6 +15,15 @@
     return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
 }
 
+async function ensurePathWithinRoot(candidatePath, allowedRootPath, argumentName) {
+    const canonicalRoot = await fs.realpath(allowedRootPath);
+    const canonicalCandidate = await fs.realpath(candidatePath);
+    if (canonicalCandidate !== canonicalRoot && !isInsideDir(canonicalCandidate, canonicalRoot)) {
+        throw new Error(`Invalid ${argumentName}: path must be inside ${canonicalRoot}`);
+    }
+    return canonicalCandidate;
+}
+
 /**
  * Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
  * Such refs always point to external schemas: they are never bundled, rewritten nor checked.
@@ -200,12 +209,17 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Restrict user-provided paths to the current working directory tree
+        const allowedRoot = await fs.realpath(process.cwd());
+        const validatedModelsDir = await ensurePathWithinRoot(absoluteModelsDir, allowedRoot, 'modelsDirectory');
+        const validatedRootPath = await ensurePathWithinRoot(absoluteRootPath, allowedRoot, 'rootSchemaPath');
 
-        console.log(`Models directory: ${absoluteModelsDir}`);
-        console.log(`Root schema: ${absoluteRootPath}`);
+        const rootSchemaFilename = path.basename(validatedRootPath);
+        const rootSchemaDir = path.dirname(validatedRootPath);
 
+        console.log(`Models directory: ${validatedModelsDir}`);
+        console.log(`Root schema: ${validatedRootPath}`);
+
         // Generate output filenames
         const baseFilename = makeSchemaName(rootSchemaFilename);
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
@@ -218,7 +230,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(validatedModelsDir);
         const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));
 
         console.log(`Found ${schemaFiles.length} schema files in models directory`);
@@ -228,7 +240,7 @@
         let detectedSchemaVersion = null;
 
         for (const file of schemaFiles) {
-            const schemaPath = path.join(absoluteModelsDir, file);
+            const schemaPath = path.join(validatedModelsDir, file);
             console.log(`  Reading ${file}...`);
 
             const content = await fs.readFile(schemaPath, 'utf8');
@@ -244,11 +256,11 @@
 
         // Read the root schema
         console.log(`\nReading root schema...`);
-        const rootContent = await fs.readFile(absoluteRootPath, 'utf8');
+        const rootContent = await fs.readFile(validatedRootPath, 'utf8');
         const rootSchema = JSON.parse(rootContent);
 
         // Add root schema to the schemas collection
-        schemas[absoluteRootPath] = rootSchema;
+        schemas[validatedRootPath] = rootSchema;
 
         // Use detected version from root schema if available
         if (rootSchema.$schema) {
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
// 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 8 minutes ago

To fix this without changing intended functionality, validate that rootSchemaPath is inside the provided modelsDirectory before deriving output paths and writing files. In general, when user input influences filesystem paths, resolve both paths to absolute canonical form and enforce a containment rule (safe root directory).

Best fix in this file:

  • In bundleSchemas(...), after resolving/access checks and before using rootSchemaDir/bundledPath, add a guard:
    • Ensure absoluteRootPath is equal to absoluteModelsDir or is inside it (isInsideDir(absoluteRootPath, absoluteModelsDir)).
    • If not, throw an error.
  • This reuses the existing isInsideDir helper and preserves existing behavior for valid invocations while preventing writes outside the models tree.

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
@@ -200,6 +200,11 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
+        // Ensure root schema path is constrained to the models directory
+        if (absoluteRootPath !== absoluteModelsDir && !isInsideDir(absoluteRootPath, absoluteModelsDir)) {
+            throw new Error(`Root schema path must be inside models directory: ${absoluteRootPath}`);
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
EOF
@@ -200,6 +200,11 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

// Ensure root schema path is constrained to the models directory
if (absoluteRootPath !== absoluteModelsDir && !isInsideDir(absoluteRootPath, absoluteModelsDir)) {
throw new Error(`Root schema path must be inside models directory: ${absoluteRootPath}`);
}

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

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 8 minutes ago

To fix this safely without changing intended behavior, validate that both user-provided paths are constrained to an expected safe root (the current working directory), after canonicalization. Specifically:

  • In bundleSchemas, compute safeRoot = await fs.realpath(process.cwd()).
  • Canonicalize user inputs:
    • absoluteModelsDir = await fs.realpath(path.resolve(modelsDirectory))
    • absoluteRootPath = await fs.realpath(path.resolve(rootSchemaPath))
  • Enforce containment with the existing isInsideDir helper (plus equality check):
    • allow when candidate === safeRoot || isInsideDir(candidate, safeRoot)
    • reject otherwise with an error.
  • Keep existing functionality (same outputs relative to provided schema path) for valid in-repo paths.

This requires edits only in tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas near lines 196–201.

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
@@ -193,9 +193,16 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const safeRoot = await fs.realpath(process.cwd());
+        const absoluteModelsDir = await fs.realpath(path.resolve(modelsDirectory));
+        const absoluteRootPath = await fs.realpath(path.resolve(rootSchemaPath));
 
+        const modelsDirAllowed = absoluteModelsDir === safeRoot || isInsideDir(absoluteModelsDir, safeRoot);
+        const rootSchemaAllowed = absoluteRootPath === safeRoot || isInsideDir(absoluteRootPath, safeRoot);
+        if (!modelsDirAllowed || !rootSchemaAllowed) {
+            throw new Error(`Input paths must be inside the working directory: ${safeRoot}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -193,9 +193,16 @@

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

const modelsDirAllowed = absoluteModelsDir === safeRoot || isInsideDir(absoluteModelsDir, safeRoot);
const rootSchemaAllowed = absoluteRootPath === safeRoot || isInsideDir(absoluteRootPath, safeRoot);
if (!modelsDirAllowed || !rootSchemaAllowed) {
throw new Error(`Input paths must be inside the 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.
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 8 minutes ago

The safest fix without changing intended functionality is to enforce that both the input root schema and generated output files remain inside the provided models directory after canonicalization. This keeps current behavior (write beside root schema) for valid inputs, while blocking dangerous paths.

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

  1. Canonicalize modelsDirectory and rootSchemaPath with fs.realpath(...) after path.resolve(...).
  2. Validate absoluteRootPath is inside absoluteModelsDir (or equal to it if desired policy; here we require “inside” as existing helper does).
  3. Build output paths as today, then canonicalize their parent dirs (fs.realpath(path.dirname(...))) and reconstruct canonical target paths.
  4. Validate canonical output targets are inside canonical models dir before writing.
  5. Throw explicit errors when checks fail.

No new dependency is required; existing path, fs.promises, and existing isInsideDir helper 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
@@ -193,13 +193,22 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        const resolvedModelsDir = path.resolve(modelsDirectory);
+        const resolvedRootPath = path.resolve(rootSchemaPath);
 
         // Verify paths exist
-        await fs.access(absoluteModelsDir);
-        await fs.access(absoluteRootPath);
+        await fs.access(resolvedModelsDir);
+        await fs.access(resolvedRootPath);
 
+        // Canonicalize paths to prevent traversal/symlink escapes
+        const absoluteModelsDir = await fs.realpath(resolvedModelsDir);
+        const absoluteRootPath = await fs.realpath(resolvedRootPath);
+
+        // Root schema must be inside the trusted models directory
+        if (!isInsideDir(absoluteRootPath, absoluteModelsDir)) {
+            throw new Error(`Root schema must be inside models directory: ${absoluteModelsDir}`);
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
@@ -211,9 +217,19 @@
         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 bundledPathCandidate = path.join(rootSchemaDir, bundledFilename);
+        const minifiedPathCandidate = path.join(rootSchemaDir, minifiedFilename);
 
+        // Canonicalize target directories and enforce output containment
+        const bundledDirCanonical = await fs.realpath(path.dirname(bundledPathCandidate));
+        const minifiedDirCanonical = await fs.realpath(path.dirname(minifiedPathCandidate));
+        const bundledPath = path.join(bundledDirCanonical, path.basename(bundledPathCandidate));
+        const minifiedPath = path.join(minifiedDirCanonical, path.basename(minifiedPathCandidate));
+
+        if (!isInsideDir(bundledPath, absoluteModelsDir) || !isInsideDir(minifiedPath, absoluteModelsDir)) {
+            throw new Error(`Output paths must be inside models directory: ${absoluteModelsDir}`);
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -193,13 +193,22 @@

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

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

// Canonicalize paths to prevent traversal/symlink escapes
const absoluteModelsDir = await fs.realpath(resolvedModelsDir);
const absoluteRootPath = await fs.realpath(resolvedRootPath);

// Root schema must be inside the trusted models directory
if (!isInsideDir(absoluteRootPath, absoluteModelsDir)) {
throw new Error(`Root schema must be inside models directory: ${absoluteModelsDir}`);
}

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

@@ -211,9 +217,19 @@
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 bundledPathCandidate = path.join(rootSchemaDir, bundledFilename);
const minifiedPathCandidate = path.join(rootSchemaDir, minifiedFilename);

// Canonicalize target directories and enforce output containment
const bundledDirCanonical = await fs.realpath(path.dirname(bundledPathCandidate));
const minifiedDirCanonical = await fs.realpath(path.dirname(minifiedPathCandidate));
const bundledPath = path.join(bundledDirCanonical, path.basename(bundledPathCandidate));
const minifiedPath = path.join(minifiedDirCanonical, path.basename(minifiedPathCandidate));

if (!isInsideDir(bundledPath, absoluteModelsDir) || !isInsideDir(minifiedPath, absoluteModelsDir)) {
throw new Error(`Output paths must be inside 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(` 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 8 minutes ago

The safest fix without changing intended functionality is to enforce that both input paths stay within an approved base directory (current working directory is a practical default), and to validate output paths as well before writing.

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

  • Add a helper to safely resolve a candidate path under a base directory and reject escapes.
  • In bundleSchemas, resolve modelsDirectory and rootSchemaPath via that helper instead of raw path.resolve(...).
  • After constructing bundledPath and minifiedPath, verify both remain inside the same safe base before any write/stat operations.
  • Keep behavior intact for normal valid project-local usage; only reject traversal/outside-base paths with a clear error.

No new dependency is required; existing path utilities and the existing isInsideDir helper 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
@@ -15,6 +15,15 @@
     return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
 }
 
+function resolvePathWithinBase(baseDir, inputPath, argumentName) {
+    const resolvedBase = path.resolve(baseDir);
+    const resolvedPath = path.resolve(resolvedBase, inputPath);
+    if (resolvedPath !== resolvedBase && !isInsideDir(resolvedPath, resolvedBase)) {
+        throw new Error(`${argumentName} must be within ${resolvedBase}`);
+    }
+    return resolvedPath;
+}
+
 /**
  * Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
  * Such refs always point to external schemas: they are never bundled, rewritten nor checked.
@@ -193,8 +202,9 @@
 
 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 = resolvePathWithinBase(safeBaseDir, modelsDirectory, 'modelsDirectory');
+        const absoluteRootPath = resolvePathWithinBase(safeBaseDir, rootSchemaPath, 'rootSchemaPath');
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
@@ -214,6 +224,13 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        if (bundledPath !== safeBaseDir && !isInsideDir(bundledPath, safeBaseDir)) {
+            throw new Error(`Refusing to write bundled schema outside ${safeBaseDir}`);
+        }
+        if (minifiedPath !== safeBaseDir && !isInsideDir(minifiedPath, safeBaseDir)) {
+            throw new Error(`Refusing to write minified schema outside ${safeBaseDir}`);
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -15,6 +15,15 @@
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
}

function resolvePathWithinBase(baseDir, inputPath, argumentName) {
const resolvedBase = path.resolve(baseDir);
const resolvedPath = path.resolve(resolvedBase, inputPath);
if (resolvedPath !== resolvedBase && !isInsideDir(resolvedPath, resolvedBase)) {
throw new Error(`${argumentName} must be within ${resolvedBase}`);
}
return resolvedPath;
}

/**
* Whether `ref` is an absolute URI (has a scheme, e.g. `https://...`, `urn:`).
* Such refs always point to external schemas: they are never bundled, rewritten nor checked.
@@ -193,8 +202,9 @@

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 = resolvePathWithinBase(safeBaseDir, modelsDirectory, 'modelsDirectory');
const absoluteRootPath = resolvePathWithinBase(safeBaseDir, rootSchemaPath, 'rootSchemaPath');

// Verify paths exist
await fs.access(absoluteModelsDir);
@@ -214,6 +224,13 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

if (bundledPath !== safeBaseDir && !isInsideDir(bundledPath, safeBaseDir)) {
throw new Error(`Refusing to write bundled schema outside ${safeBaseDir}`);
}
if (minifiedPath !== safeBaseDir && !isInsideDir(minifiedPath, safeBaseDir)) {
throw new Error(`Refusing to write minified schema outside ${safeBaseDir}`);
}

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 8 commits March 26, 2026 23:29
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@owasp.org>
Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
…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>
bhess and others added 30 commits September 9, 2026 13:48
selectedBy now only names the mechanism: protocol-fixed, build-time,
configuration, negotiation, or unknown. The new optional selectingParty
names who selects (client, server, initiator, responder, ...), which
also covers multi-party protocols. server-selected and hardware are
dropped.

Also documents the registry join (exact match on family and version)
and the algorithm name resolution rule.

Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
protocolProperties.type plus version identify the registry entry by
exact match. Adds a valid and an invalid test case for the protocol
type.

Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Basil Hess <bhe@zurich.ibm.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>
# 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>

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