Skip to content

[WIP] CycloneDX v2.0 Specification - #652

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

[WIP] CycloneDX v2.0 Specification#652
stevespringett wants to merge 268 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
petra-dv and others added 7 commits November 23, 2025 22:33
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
// 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 ']('.
Signed-off-by: Steve Springett <steve@springett.us>
const absoluteRootPath = path.resolve(rootSchemaPath);

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

The best fix is to validate CLI-supplied paths against an explicit trusted base directory before any filesystem access. Keep current behavior (accept relative or absolute inputs) but enforce that both modelsDirectory and rootSchemaPath resolve inside a chosen safe root (here: process.cwd(), which is a sensible default for CLI execution). This prevents path traversal / arbitrary path targeting while preserving normal usage from project root.

In tools/src/main/js/bundler/bundle-schemas.js, update bundleSchemas so that:

  1. Compute safeRoot = path.resolve(process.cwd()).
  2. Resolve both inputs relative to that root: path.resolve(safeRoot, input).
  3. Add a helper check that ensures resolved paths are within safeRoot using path.relative and rejecting absolute/outside results.
  4. Throw a clear error before calling fs.access if validation fails.

No new dependencies are required; use built-in path utilities 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 safeRoot = path.resolve(process.cwd());
+        const absoluteModelsDir = path.resolve(safeRoot, modelsDirectory);
+        const absoluteRootPath = path.resolve(safeRoot, rootSchemaPath);
 
+        const isPathWithinRoot = (targetPath, rootPath) => {
+            const rel = path.relative(rootPath, targetPath);
+            return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
+        };
+
+        if (!isPathWithinRoot(absoluteModelsDir, safeRoot) || !isPathWithinRoot(absoluteRootPath, safeRoot)) {
+            throw new Error(`Input paths must be within the working directory: ${safeRoot}`);
+        }
+
         // 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 safeRoot = path.resolve(process.cwd());
const absoluteModelsDir = path.resolve(safeRoot, modelsDirectory);
const absoluteRootPath = path.resolve(safeRoot, rootSchemaPath);

const isPathWithinRoot = (targetPath, rootPath) => {
const rel = path.relative(rootPath, targetPath);
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
};

if (!isPathWithinRoot(absoluteModelsDir, safeRoot) || !isPathWithinRoot(absoluteRootPath, safeRoot)) {
throw new Error(`Input paths must be within 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.

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

To fix this safely without changing intended behavior too much, validate that the user-provided rootSchemaPath resolves to a file inside the provided modelsDirectory (the natural safe root in this script). Keep path.resolve, then add a containment check using path.relative (safer than naive startsWith for cross-platform behavior). Reject any path that is outside the models directory or resolves to a different drive/root on Windows.

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

  1. In bundleSchemas right after:
    • const absoluteModelsDir = path.resolve(modelsDirectory);
    • const absoluteRootPath = path.resolve(rootSchemaPath);
  2. Add:
    • const relativeRootToModels = path.relative(absoluteModelsDir, absoluteRootPath);
    • guard that throws if relativeRootToModels starts with '..', is absolute, or (optionally) equals '' (directory itself).
  3. Keep existing fs.access checks after validation.

No new imports or dependencies are 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
@@ -182,6 +182,15 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        // Ensure the root schema path stays within the models directory.
+        const relativeRootToModels = path.relative(absoluteModelsDir, absoluteRootPath);
+        if (
+            relativeRootToModels.startsWith('..') ||
+            path.isAbsolute(relativeRootToModels)
+        ) {
+            throw new Error(`Root schema path must be within models directory: ${absoluteModelsDir}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -182,6 +182,15 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

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

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

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

The best fix is to constrain both CLI-provided paths to a trusted root directory after normalization, and reject anything outside that root.
In this file, a practical trusted root is process.cwd() for CLI execution (current project/workspace). This preserves existing behavior for normal relative project paths, while blocking unexpected absolute/out-of-tree paths.

What to change

In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas(...) right after:

  • const absoluteModelsDir = path.resolve(modelsDirectory);
  • const absoluteRootPath = path.resolve(rootSchemaPath);

add:

  1. A trustedRoot derived from path.resolve(process.cwd()).
  2. A helper check using path.relative(trustedRoot, candidate) to ensure candidate is inside root.
  3. Throw an error if either resolved path is outside trusted root.

This avoids relying on string prefix tricks and works cross-platform.

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
@@ -181,7 +181,17 @@
     try {
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
+        const trustedRoot = path.resolve(process.cwd());
 
+        const isPathWithinRoot = (candidatePath) => {
+            const relativePath = path.relative(trustedRoot, candidatePath);
+            return relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
+        };
+
+        if (!isPathWithinRoot(absoluteModelsDir) || !isPathWithinRoot(absoluteRootPath)) {
+            throw new Error(`Input paths must be within project directory: ${trustedRoot}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -181,7 +181,17 @@
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const trustedRoot = path.resolve(process.cwd());

const isPathWithinRoot = (candidatePath) => {
const relativePath = path.relative(trustedRoot, candidatePath);
return relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
};

if (!isPathWithinRoot(absoluteModelsDir) || !isPathWithinRoot(absoluteRootPath)) {
throw new Error(`Input paths must be within project directory: ${trustedRoot}`);
}

// 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 about 11 hours ago

Use a safe-root containment check for user-provided paths before any filesystem operations. The best fix here is to define an allowed base directory (current working directory is a practical default for this CLI), normalize both CLI paths with path.resolve, then reject execution unless both resolved paths stay inside that base. Also use fs.realpath to canonicalize symlinks before the containment check to avoid symlink traversal bypasses.

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

  • In bundleSchemas(...) near lines 182–187, after resolving input paths, add:
    • const allowedBaseDir = path.resolve(process.cwd());
    • a small helper to test containment safely.
    • await fs.realpath(...) for both absoluteModelsDir and absoluteRootPath.
    • throw an error if either canonical path is outside allowedBaseDir.
  • Keep existing behavior otherwise (same outputs, same schema processing), only adding validation gates before readdir/readFile.

No new dependency is 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
@@ -181,14 +181,31 @@
     try {
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
+        const allowedBaseDir = path.resolve(process.cwd());
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Resolve symlinks/canonical paths and enforce allowed base directory containment
+        const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
+        const canonicalRootPath = await fs.realpath(absoluteRootPath);
 
+        const isPathWithin = (baseDir, targetPath) => {
+            const relative = path.relative(baseDir, targetPath);
+            return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
+        };
+
+        if (
+            !isPathWithin(allowedBaseDir, canonicalModelsDir) ||
+            !isPathWithin(allowedBaseDir, canonicalRootPath)
+        ) {
+            throw new Error(`Input paths must be within the working directory: ${allowedBaseDir}`);
+        }
+
+        const rootSchemaFilename = path.basename(canonicalRootPath);
+        const rootSchemaDir = path.dirname(canonicalRootPath);
+
         console.log(`Models directory: ${absoluteModelsDir}`);
         console.log(`Root schema: ${absoluteRootPath}`);
 
EOF
@@ -181,14 +181,31 @@
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
const allowedBaseDir = path.resolve(process.cwd());

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

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Resolve symlinks/canonical paths and enforce allowed base directory containment
const canonicalModelsDir = await fs.realpath(absoluteModelsDir);
const canonicalRootPath = await fs.realpath(absoluteRootPath);

const isPathWithin = (baseDir, targetPath) => {
const relative = path.relative(baseDir, targetPath);
return relative && !relative.startsWith('..') && !path.isAbsolute(relative);
};

if (
!isPathWithin(allowedBaseDir, canonicalModelsDir) ||
!isPathWithin(allowedBaseDir, canonicalRootPath)
) {
throw new Error(`Input paths must be within the working directory: ${allowedBaseDir}`);
}

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

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

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

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

To fix this without changing intended functionality, constrain user-provided paths to a safe base directory (current working directory) after canonicalization. Specifically:

  • In bundleSchemas, canonicalize modelsDirectory and rootSchemaPath with path.resolve.
  • Canonicalize a trusted base root (e.g., process.cwd()).
  • Verify both resolved paths are within that base root using path.relative(...) and reject paths that escape (.. prefix or absolute relative result).
  • Keep existing fs.access checks and subsequent logic unchanged.

This is the safest minimal change in tools/src/main/js/bundler/bundle-schemas.js around lines 180–188, requiring no new dependencies and preserving current behavior for normal in-repo usage.

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 allowedRoot = path.resolve(process.cwd());
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        const modelsRelative = path.relative(allowedRoot, absoluteModelsDir);
+        const rootRelative = path.relative(allowedRoot, absoluteRootPath);
+        const modelsOutsideRoot = modelsRelative.startsWith('..') || path.isAbsolute(modelsRelative);
+        const rootOutsideRoot = rootRelative.startsWith('..') || path.isAbsolute(rootRelative);
+
+        if (modelsOutsideRoot || rootOutsideRoot) {
+            throw new Error(`Input paths must be within the working directory: ${allowedRoot}`);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
EOF
@@ -179,9 +179,19 @@

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

const modelsRelative = path.relative(allowedRoot, absoluteModelsDir);
const rootRelative = path.relative(allowedRoot, absoluteRootPath);
const modelsOutsideRoot = modelsRelative.startsWith('..') || path.isAbsolute(modelsRelative);
const rootOutsideRoot = rootRelative.startsWith('..') || path.isAbsolute(rootRelative);

if (modelsOutsideRoot || rootOutsideRoot) {
throw new Error(`Input paths must be within the working directory: ${allowedRoot}`);
}

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

To fix this safely without changing intended functionality, validate that output paths remain within an allowed base directory derived from the provided models directory.
General approach: normalize paths, compute candidate output paths, then enforce containment using path.relative (or prefix checks on normalized absolute paths) before writing files.

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

  • In bundleSchemas, after resolving absoluteModelsDir and absoluteRootPath, compute allowedOutputRoot = absoluteModelsDir.
  • Validate absoluteRootPath is inside allowedOutputRoot (prevents choosing a root schema outside safe workspace).
  • After computing bundledPath and minifiedPath, validate each is still within allowedOutputRoot.
  • If validation fails, throw an error and abort.
  • No new dependency is required; use built-in path.relative.

This keeps existing behavior for valid inputs while preventing path traversal / arbitrary write 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
@@ -186,6 +186,13 @@
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
 
+        // Constrain all reads/writes to stay under the models directory
+        const allowedOutputRoot = absoluteModelsDir;
+        const rootPathRelative = path.relative(allowedOutputRoot, absoluteRootPath);
+        if (rootPathRelative.startsWith('..') || path.isAbsolute(rootPathRelative)) {
+            throw new Error(`Root schema path must be within models directory: ${allowedOutputRoot}`);
+        }
+
         const rootSchemaFilename = path.basename(absoluteRootPath);
         const rootSchemaDir = path.dirname(absoluteRootPath);
 
@@ -200,6 +207,15 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        const bundledRelative = path.relative(allowedOutputRoot, bundledPath);
+        const minifiedRelative = path.relative(allowedOutputRoot, minifiedPath);
+        if (
+            bundledRelative.startsWith('..') || path.isAbsolute(bundledRelative) ||
+            minifiedRelative.startsWith('..') || path.isAbsolute(minifiedRelative)
+        ) {
+            throw new Error(`Refusing to write output outside models directory: ${allowedOutputRoot}`);
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -186,6 +186,13 @@
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

// Constrain all reads/writes to stay under the models directory
const allowedOutputRoot = absoluteModelsDir;
const rootPathRelative = path.relative(allowedOutputRoot, absoluteRootPath);
if (rootPathRelative.startsWith('..') || path.isAbsolute(rootPathRelative)) {
throw new Error(`Root schema path must be within models directory: ${allowedOutputRoot}`);
}

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

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

const bundledRelative = path.relative(allowedOutputRoot, bundledPath);
const minifiedRelative = path.relative(allowedOutputRoot, minifiedPath);
if (
bundledRelative.startsWith('..') || path.isAbsolute(bundledRelative) ||
minifiedRelative.startsWith('..') || path.isAbsolute(minifiedRelative)
) {
throw new Error(`Refusing to write output outside models directory: ${allowedOutputRoot}`);
}

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

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

The best fix is to enforce that all computed output paths remain inside the normalized models directory (safe root), and reject execution otherwise. This preserves existing functionality for valid inputs while preventing path traversal / arbitrary write locations from user-provided rootSchemaPath.

In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas(...) right after computing bundledPath and minifiedPath, add a containment check:

  1. Normalize the trusted root with a trailing separator (e.g., absoluteModelsDir + path.sep).
  2. Normalize candidate output paths with path.resolve(...).
  3. Verify each output path is either exactly the root directory or starts with the root+separator.
  4. Throw an error if either path escapes the root.

This requires no new dependency and no import changes.

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,20 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        // Ensure output files remain within the models directory
+        const safeRoot = path.resolve(absoluteModelsDir);
+        const safeRootWithSep = safeRoot.endsWith(path.sep) ? safeRoot : `${safeRoot}${path.sep}`;
+        const resolvedBundledPath = path.resolve(bundledPath);
+        const resolvedMinifiedPath = path.resolve(minifiedPath);
+        const bundledInSafeRoot =
+            resolvedBundledPath === safeRoot || resolvedBundledPath.startsWith(safeRootWithSep);
+        const minifiedInSafeRoot =
+            resolvedMinifiedPath === safeRoot || resolvedMinifiedPath.startsWith(safeRootWithSep);
+
+        if (!bundledInSafeRoot || !minifiedInSafeRoot) {
+            throw new Error('Output schema paths must be within the models directory');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -200,6 +200,20 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

// Ensure output files remain within the models directory
const safeRoot = path.resolve(absoluteModelsDir);
const safeRootWithSep = safeRoot.endsWith(path.sep) ? safeRoot : `${safeRoot}${path.sep}`;
const resolvedBundledPath = path.resolve(bundledPath);
const resolvedMinifiedPath = path.resolve(minifiedPath);
const bundledInSafeRoot =
resolvedBundledPath === safeRoot || resolvedBundledPath.startsWith(safeRootWithSep);
const minifiedInSafeRoot =
resolvedMinifiedPath === safeRoot || resolvedMinifiedPath.startsWith(safeRootWithSep);

if (!bundledInSafeRoot || !minifiedInSafeRoot) {
throw new Error('Output schema paths must be within the models directory');
}

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

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

await fs.writeFile(minifiedPath, minifiedJson);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

The best fix is to constrain all file operations to a trusted base directory, then verify normalized paths remain inside that base before read/write. Here, the natural trusted root is modelsDirectory (already a required input and intended schema workspace). Keep existing behavior, but add a containment check after resolving paths:

  1. Resolve and normalize:

    • absoluteModelsDir = path.resolve(modelsDirectory)
    • absoluteRootPath = path.resolve(rootSchemaPath)
  2. Enforce absoluteRootPath is inside absoluteModelsDir using path.relative:

    • reject if relativePath is absolute or starts with ..
  3. Build output paths and re-check containment for bundledPath and minifiedPath the same way before writing.

This change should be made in tools/src/main/js/bundler/bundle-schemas.js within bundleSchemas(...), near lines 182–201, adding a small helper function in the same file (above bundleSchemas) to avoid duplicate 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
@@ -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 }
  */
@@ -182,6 +187,10 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        if (!isPathInside(absoluteModelsDir, absoluteRootPath)) {
+            throw new Error('Root schema path must be within the models directory.');
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
@@ -197,9 +206,13 @@
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
         const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;
 
-        const bundledPath = path.join(rootSchemaDir, bundledFilename);
-        const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
+        const bundledPath = path.resolve(rootSchemaDir, bundledFilename);
+        const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename);
 
+        if (!isPathInside(absoluteModelsDir, bundledPath) || !isPathInside(absoluteModelsDir, minifiedPath)) {
+            throw new Error('Output schema paths must be within the models directory.');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
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 }
*/
@@ -182,6 +187,10 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

if (!isPathInside(absoluteModelsDir, absoluteRootPath)) {
throw new Error('Root schema path must be within the models directory.');
}

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
@@ -197,9 +206,13 @@
const bundledFilename = `${baseFilename}-bundled.schema.json`;
const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;

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

if (!isPathInside(absoluteModelsDir, bundledPath) || !isPathInside(absoluteModelsDir, minifiedPath)) {
throw new Error('Output schema paths must be within the models directory.');
}

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

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

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

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI about 11 hours ago

To fix this safely without changing intended functionality, validate that the computed output files remain within the same directory as the resolved root schema path before writing/stat-ing them. In general: resolve and normalize all relevant paths, then enforce a root boundary check using path.relative (or equivalent) to reject traversal/out-of-root targets.

Best fix in this file:

  • In bundleSchemas (around lines 200–201 where bundledPath and minifiedPath are built), compute resolved output paths and verify both are contained in rootSchemaDir.
  • Add a small helper function (near existing helpers) to check directory containment robustly:
    • const relative = path.relative(baseDir, candidatePath)
    • Reject if relative is empty? (empty is fine for same path), starts with .., or path.isAbsolute(relative).
  • Throw an error if validation fails, before any file write/read on those paths.
  • Keep existing behavior (outputs next to root schema) intact.

No new dependencies are needed.

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

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundler/bundle-schemas.js b/tools/src/main/js/bundler/bundle-schemas.js
--- a/tools/src/main/js/bundler/bundle-schemas.js
+++ b/tools/src/main/js/bundler/bundle-schemas.js
@@ -16,6 +16,11 @@
     return typeof value === 'object' && value !== null;
 }
 
+function isPathWithinDirectory(baseDir, targetPath) {
+    const relative = path.relative(baseDir, targetPath);
+    return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
+}
+
 /**
  * Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
  */
@@ -197,9 +202,13 @@
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
         const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;
 
-        const bundledPath = path.join(rootSchemaDir, bundledFilename);
-        const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
+        const bundledPath = path.resolve(rootSchemaDir, bundledFilename);
+        const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename);
 
+        if (!isPathWithinDirectory(rootSchemaDir, bundledPath) || !isPathWithinDirectory(rootSchemaDir, minifiedPath)) {
+            throw new Error('Resolved output path escapes the root schema directory');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -16,6 +16,11 @@
return typeof value === 'object' && value !== null;
}

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

/**
* Resolve a JSON Pointer (RFC6901) against an object. Returns { ok: boolean, value?: any, error?: string }
*/
@@ -197,9 +202,13 @@
const bundledFilename = `${baseFilename}-bundled.schema.json`;
const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;

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

if (!isPathWithinDirectory(rootSchemaDir, bundledPath) || !isPathWithinDirectory(rootSchemaDir, minifiedPath)) {
throw new Error('Resolved output path escapes the root schema directory');
}

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

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

Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
github-actions Bot and others added 10 commits August 9, 2026 21:06
Co-authored-by: Jan Kowalleck <jan.kowalleck@owasp.org>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
move those docgen for schema-v1 to own dir, tp path the way for new docs
tools
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
},
"keywords": ["json-schema", "ajv", "bundle"],
"author": "",
"license": "MIT",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MIT? not Apache-2.0 with NOTICE file?

jkowalleck and others added 19 commits August 15, 2026 09:53
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Steve Springett <steve@springett.us>
Co-authored-by: Jan Kowalleck <jan.kowalleck@owasp.org>
Signed-off-by: Steve Springett <steve@springett.us>
Co-authored-by: Jan Kowalleck <jan.kowalleck@owasp.org>
Signed-off-by: Steve Springett <steve@springett.us>


Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-changes CDX 2.0 related to release v2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CycloneDX 2.0

8 participants