Repository navigation
[WIP] CycloneDX v2.0 Specification - #652
stevespringett wants to merge 384 commits into
Conversation
| // 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
| // 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
|
|
||
| // Verify paths exist | ||
| await fs.access(absoluteModelsDir); | ||
| await fs.access(absoluteRootPath); |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
To fix this safely without changing intended functionality, validate that the provided rootSchemaPath is contained within the provided modulesDirectory (or within an expected safe root), after canonicalization. The most robust approach here is:
- Resolve both input paths to absolute paths (
path.resolve) — already done. - Canonicalize them with
fs.realpathto collapse symlinks. - Enforce containment (
rootmust be equal to or under modules directory) before any further filesystem use. - Fail fast with a clear error if validation fails.
This should be implemented in tools/src/main/js/bundler/bundle-schemas.js inside bundleSchemas, right after initial resolve/access and before using the root schema path further. Also slightly adjust isInsideDir so it treats the directory itself as “inside” (needed for robust boundary checks).
| @@ -12,7 +12,7 @@ | ||
| */ | ||
| function isInsideDir(filePath, dirPath) { | ||
| const rel = path.relative(dirPath, filePath); | ||
| return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel); | ||
| return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel)); | ||
| } | ||
|
|
||
| /** | ||
| @@ -200,12 +200,19 @@ | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); | ||
|
|
||
| const rootSchemaFilename = path.basename(absoluteRootPath); | ||
| const rootSchemaDir = path.dirname(absoluteRootPath); | ||
| // Canonicalize to prevent traversal via symlinks and enforce trust boundary. | ||
| const canonicalModulesDir = await fs.realpath(absoluteModulesDir); | ||
| const canonicalRootPath = await fs.realpath(absoluteRootPath); | ||
| if (!isInsideDir(canonicalRootPath, canonicalModulesDir)) { | ||
| throw new Error(`Root schema path must be inside modules directory: ${canonicalModulesDir}`); | ||
| } | ||
|
|
||
| console.log(`Modules directory: ${absoluteModulesDir}`); | ||
| console.log(`Root schema: ${absoluteRootPath}`); | ||
| const rootSchemaFilename = path.basename(canonicalRootPath); | ||
| const rootSchemaDir = path.dirname(canonicalRootPath); | ||
|
|
||
| console.log(`Modules directory: ${canonicalModulesDir}`); | ||
| console.log(`Root schema: ${canonicalRootPath}`); | ||
|
|
||
| // Generate output filenames | ||
| const baseFilename = makeSchemaName(rootSchemaFilename); | ||
| const bundledFilename = `${baseFilename}-bundled.schema.json`; |
| 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
To fix this safely without changing intended behavior, validate and constrain all filesystem access to an allowed root directory after canonicalization (resolve symlinks), then ensure both:
modulesDirectoryis under that safe root, and- every computed
schemaPathis still inside the validated modules directory.
In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas:
- Canonicalize
modulesDirectoryandrootSchemaPathwithfs.realpath(...)afterfs.access(...). - Derive a safe boundary directory (best fit here: current working directory via
process.cwd()canonicalized). - Reject paths not inside that boundary using existing
isInsideDir(...)(extended to allow equality for directory checks). - In the loop, canonicalize each
schemaPathbefore reading and verify it is inside the canonical modules directory to prevent symlink escape.
No new dependencies are needed.
| @@ -10,9 +10,9 @@ | ||
| /** | ||
| * Whether `filePath` lives (at any depth) inside `dirPath`. Both must be absolute. | ||
| */ | ||
| function isInsideDir(filePath, dirPath) { | ||
| function isInsideDir(filePath, dirPath, allowEqual = false) { | ||
| const rel = path.relative(dirPath, filePath); | ||
| return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel); | ||
| return (allowEqual && rel === '') || (rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel)); | ||
| } | ||
|
|
||
| /** | ||
| @@ -200,9 +199,21 @@ | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); | ||
|
|
||
| const rootSchemaFilename = path.basename(absoluteRootPath); | ||
| const rootSchemaDir = path.dirname(absoluteRootPath); | ||
| // Canonicalize user-provided paths (resolves symlinks) and enforce they stay under a safe root. | ||
| const safeRootDir = await fs.realpath(process.cwd()); | ||
| const canonicalModulesDir = await fs.realpath(absoluteModulesDir); | ||
| const canonicalRootPath = await fs.realpath(absoluteRootPath); | ||
|
|
||
| if (!isInsideDir(canonicalModulesDir, safeRootDir, true)) { | ||
| throw new Error(`Modules directory must be within working directory: ${safeRootDir}`); | ||
| } | ||
| if (!isInsideDir(canonicalRootPath, safeRootDir, true)) { | ||
| throw new Error(`Root schema path must be within working directory: ${safeRootDir}`); | ||
| } | ||
|
|
||
| const rootSchemaFilename = path.basename(canonicalRootPath); | ||
| const rootSchemaDir = path.dirname(canonicalRootPath); | ||
|
|
||
| console.log(`Modules directory: ${absoluteModulesDir}`); | ||
| console.log(`Root schema: ${absoluteRootPath}`); | ||
|
|
||
| @@ -218,7 +228,7 @@ | ||
| console.log(`Output (minified): ${minifiedPath}\n`); | ||
|
|
||
| // Read all schema files in the modules directory | ||
| const files = await fs.readdir(absoluteModulesDir); | ||
| const files = await fs.readdir(canonicalModulesDir); | ||
| const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled')); | ||
|
|
||
| console.log(`Found ${schemaFiles.length} schema files in modules directory`); | ||
| @@ -228,10 +238,14 @@ | ||
| let detectedSchemaVersion = null; | ||
|
|
||
| for (const file of schemaFiles) { | ||
| const schemaPath = path.join(absoluteModulesDir, file); | ||
| const schemaPath = path.join(canonicalModulesDir, file); | ||
| const canonicalSchemaPath = await fs.realpath(schemaPath); | ||
| if (!isInsideDir(canonicalSchemaPath, canonicalModulesDir, true)) { | ||
| throw new Error(`Schema file resolves outside modules directory: ${schemaPath}`); | ||
| } | ||
| console.log(` Reading ${file}...`); | ||
|
|
||
| const content = await fs.readFile(schemaPath, 'utf8'); | ||
| const content = await fs.readFile(canonicalSchemaPath, 'utf8'); | ||
| const schema = JSON.parse(content); | ||
|
|
||
| // Detect the $schema version from the first schema that has it |
|
|
||
| // 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
To fix this safely without changing intended bundling behavior, validate that the resolved root schema path is inside an allowed base directory before using it. The best fit in this file is to constrain absoluteRootPath to the parent directory of absoluteModulesDir (which matches the shown CLI example layout: .../schema/<ver>/modules and .../schema/<ver>/root.schema.json).
What to change (tools/src/main/js/bundler/bundle-schemas.js):
- In
bundleSchemas, after computingabsoluteModulesDirandabsoluteRootPath, computeallowedRootBaseDir = path.dirname(absoluteModulesDir). - Use existing
isInsideDirhelper (plus equality check) to enforce:absoluteRootPath === allowedRootBaseDirOR inside it.
- Throw an error if validation fails, before any file access.
- Keep all existing behavior otherwise unchanged.
No new dependency is required; existing path and helper function are sufficient.
| @@ -195,7 +195,14 @@ | ||
| try { | ||
| const absoluteModulesDir = path.resolve(modulesDirectory); | ||
| const absoluteRootPath = path.resolve(rootSchemaPath); | ||
| const allowedRootBaseDir = path.dirname(absoluteModulesDir); | ||
|
|
||
| // Ensure the root schema path stays within the expected schema root directory | ||
| // (for example: <schema-version>/modules + <schema-version>/root.schema.json) | ||
| if (absoluteRootPath !== allowedRootBaseDir && !isInsideDir(absoluteRootPath, allowedRootBaseDir)) { | ||
| throw new Error(`Root schema path must be inside: ${allowedRootBaseDir}`); | ||
| } | ||
|
|
||
| // Verify paths exist | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); |
| // 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
To fix this without changing normal functionality, constrain all file writes to a trusted workspace root (here: modulesDirectory) and reject a rootSchemaPath that is outside that root.
Best approach in this file:
- Compute canonical absolute paths for both
modulesDirectoryandrootSchemaPathusingfs.realpathafter existence checks (resolves symlinks too). - Enforce containment: ensure canonical root schema path is inside canonical modules directory (or equal to it if desired).
- Build output paths from the canonical root schema directory and re-check the final output paths are inside canonical modules directory before writing.
This requires only edits in tools/src/main/js/bundler/bundle-schemas.js inside bundleSchemas, reusing existing isInsideDir helper and existing imports (fs, path).
| @@ -200,9 +200,18 @@ | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); | ||
|
|
||
| const rootSchemaFilename = path.basename(absoluteRootPath); | ||
| const rootSchemaDir = path.dirname(absoluteRootPath); | ||
| // Canonicalize paths to account for symlinks before containment checks | ||
| const canonicalModulesDir = await fs.realpath(absoluteModulesDir); | ||
| const canonicalRootPath = await fs.realpath(absoluteRootPath); | ||
|
|
||
| // Security check: root schema must be inside modules directory | ||
| if (canonicalRootPath !== canonicalModulesDir && !isInsideDir(canonicalRootPath, canonicalModulesDir)) { | ||
| throw new Error(`Root schema path must be inside modules directory. root=${canonicalRootPath}, modules=${canonicalModulesDir}`); | ||
| } | ||
|
|
||
| const rootSchemaFilename = path.basename(canonicalRootPath); | ||
| const rootSchemaDir = path.dirname(canonicalRootPath); | ||
|
|
||
| console.log(`Modules directory: ${absoluteModulesDir}`); | ||
| console.log(`Root schema: ${absoluteRootPath}`); | ||
|
|
||
| @@ -214,6 +222,10 @@ | ||
| const bundledPath = path.join(rootSchemaDir, bundledFilename); | ||
| const minifiedPath = path.join(rootSchemaDir, minifiedFilename); | ||
|
|
||
| if (!isInsideDir(bundledPath, canonicalModulesDir) || !isInsideDir(minifiedPath, canonicalModulesDir)) { | ||
| throw new Error('Refusing to write output files outside modules directory'); | ||
| } | ||
|
|
||
| console.log(`Output (bundled): ${bundledPath}`); | ||
| console.log(`Output (minified): ${minifiedPath}\n`); | ||
|
|
| 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
To fix this without changing intended behavior, validate that:
rootSchemaPathis insidemodulesDirectory(or at least within a defined safe root), and- generated output files (
bundledPath,minifiedPath) are also inside that same safe root.
Best concrete fix in tools/src/main/js/bundler/bundle-schemas.js:
- Keep existing
path.resolve. - Add containment checks using the existing
isInsideDirhelper. - Also allow equality for directory checks (because current helper returns false when paths are equal).
- Throw a clear error if any check fails before any file write/stat happens.
This preserves functionality for legitimate inputs while preventing traversal/out-of-scope writes.
| @@ -200,6 +200,11 @@ | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); | ||
|
|
||
| // Constrain user-provided paths to the modules directory | ||
| if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) { | ||
| throw new Error(`Root schema path must be inside modules directory: ${absoluteRootPath}`); | ||
| } | ||
|
|
||
| const rootSchemaFilename = path.basename(absoluteRootPath); | ||
| const rootSchemaDir = path.dirname(absoluteRootPath); | ||
|
|
||
| @@ -211,9 +216,13 @@ | ||
| const bundledFilename = `${baseFilename}-bundled.schema.json`; | ||
| const minifiedFilename = `${baseFilename}-bundled.min.schema.json`; | ||
|
|
||
| const bundledPath = path.join(rootSchemaDir, bundledFilename); | ||
| const minifiedPath = path.join(rootSchemaDir, minifiedFilename); | ||
| const bundledPath = path.resolve(rootSchemaDir, bundledFilename); | ||
| const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename); | ||
|
|
||
| if (!isInsideDir(bundledPath, absoluteModulesDir) || !isInsideDir(minifiedPath, absoluteModulesDir)) { | ||
| throw new Error('Refusing to write output files outside modules directory'); | ||
| } | ||
|
|
||
| console.log(`Output (bundled): ${bundledPath}`); | ||
| console.log(`Output (minified): ${minifiedPath}\n`); | ||
|
|
| 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
Use a containment check to ensure all write targets remain under an approved base directory, and fail fast otherwise. The best minimal fix here is to enforce that rootSchemaPath is inside modulesDirectory (or at least that generated output paths are), then validate bundledPath and minifiedPath with the existing isInsideDir helper before any write.
In tools/src/main/js/bundler/bundle-schemas.js, inside bundleSchemas(...) after computing absolute paths and before generating/using output paths:
- Ensure
absoluteRootPathis underabsoluteModulesDir(or equal if desired). - After creating
bundledPath/minifiedPath, resolve them and verify they are insideabsoluteModulesDir. - Throw an error if checks fail.
No new dependency is needed; reuse existing path and isInsideDir.
| @@ -200,6 +200,11 @@ | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); | ||
|
|
||
| // Enforce that root schema and generated outputs stay within the modules directory | ||
| if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) { | ||
| throw new Error(`Root schema path must be inside modules directory: ${absoluteRootPath}`); | ||
| } | ||
|
|
||
| const rootSchemaFilename = path.basename(absoluteRootPath); | ||
| const rootSchemaDir = path.dirname(absoluteRootPath); | ||
|
|
||
| @@ -211,9 +216,16 @@ | ||
| const bundledFilename = `${baseFilename}-bundled.schema.json`; | ||
| const minifiedFilename = `${baseFilename}-bundled.min.schema.json`; | ||
|
|
||
| const bundledPath = path.join(rootSchemaDir, bundledFilename); | ||
| const minifiedPath = path.join(rootSchemaDir, minifiedFilename); | ||
| const bundledPath = path.resolve(rootSchemaDir, bundledFilename); | ||
| const minifiedPath = path.resolve(rootSchemaDir, minifiedFilename); | ||
|
|
||
| if ( | ||
| (bundledPath !== absoluteModulesDir && !isInsideDir(bundledPath, absoluteModulesDir)) || | ||
| (minifiedPath !== absoluteModulesDir && !isInsideDir(minifiedPath, absoluteModulesDir)) | ||
| ) { | ||
| throw new Error('Refusing to write bundled schemas outside modules directory'); | ||
| } | ||
|
|
||
| console.log(`Output (bundled): ${bundledPath}`); | ||
| console.log(`Output (minified): ${minifiedPath}\n`); | ||
|
|
| 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
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 days ago
General fix: constrain user-controlled paths to a trusted base directory and reject any resolved path that escapes that base. Normalization alone is insufficient; enforce containment after resolve.
Best fix here without changing intended functionality too much:
- In
bundleSchemas(around lines 196–205), derive a trusted base frommodulesDirectory(already a required input), resolverootSchemaPathrelative to it, and reject if the resulting absolute path is not inside that base. - Keep existing behavior for generating outputs next to root schema, but now guaranteed under trusted root.
- Use existing
isInsideDirhelper (already present) plus equality check for exact base match. - No new dependencies needed.
Concretely in tools/src/main/js/bundler/bundle-schemas.js:
- Replace current
absoluteRootPath = path.resolve(rootSchemaPath)with:absoluteModulesDir = path.resolve(modulesDirectory);absoluteRootPath = path.resolve(absoluteModulesDir, rootSchemaPath);- containment check:
absoluteRootPath === absoluteModulesDir || isInsideDir(absoluteRootPath, absoluteModulesDir) - throw on failure.
- Leave downstream path construction unchanged.
| @@ -194,8 +194,12 @@ | ||
| async function bundleSchemas(modulesDirectory, rootSchemaPath, options = {}) { | ||
| try { | ||
| const absoluteModulesDir = path.resolve(modulesDirectory); | ||
| const absoluteRootPath = path.resolve(rootSchemaPath); | ||
| const absoluteRootPath = path.resolve(absoluteModulesDir, rootSchemaPath); | ||
|
|
||
| if (absoluteRootPath !== absoluteModulesDir && !isInsideDir(absoluteRootPath, absoluteModulesDir)) { | ||
| throw new Error(`Root schema path must be inside modules directory: ${rootSchemaPath}`); | ||
| } | ||
|
|
||
| // Verify paths exist | ||
| await fs.access(absoluteModulesDir); | ||
| await fs.access(absoluteRootPath); |
f63bd4a to
9a09935
Compare
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>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Basil Hess <bhe@zurich.ibm.com>
Signed-off-by: Steve Springett <steve@springett.us>
…ed schemas. Supersedes and closes #777. Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Steve Springett <steve@springett.us>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
# Conflicts: # schema/2.0/model/cyclonedx-cryptography-2.0.schema.json
- End all protocolFamiliesEnum meta:enum descriptions with a full stop - Drop the redundant "type" keyword from enums in cryptography-defs, in line with the const/enum refactor on 2.0-dev - Add the missing newline at end of cryptography-defs.schema.json - Rename the protocolProperties.type title to "Protocol Type"
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
Signed-off-by: Steve Springett <steve@springett.us>
# Conflicts: # .github/workflows/test_1.x_php.yml
Important
WORK IN PROGRESS
see Milestone for progress: https://github.com/CycloneDX/specification/milestone/2
BREAKING Changes
To be explained further.
Reasoning: Downstream spec users may build ontop of JSON schema.
To be explained further.
... TBC ...
Added
... TBD ...
Chaned
... TBD ...
Removed
... TBD ...
Misc
... TBD ...