@cyclonedx/cdxgen 12.1.2 → 12.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +11 -9
  2. package/bin/cdxgen.js +1 -1
  3. package/lib/cli/index.js +9 -5
  4. package/lib/evinser/evinser.js +2 -8
  5. package/lib/helpers/display.js +1 -1
  6. package/lib/helpers/envcontext.js +10 -2
  7. package/lib/helpers/utils.js +462 -86
  8. package/lib/helpers/utils.poku.js +179 -2
  9. package/lib/helpers/validator.js +8 -5
  10. package/lib/managers/docker.getConnection.poku.js +61 -0
  11. package/lib/managers/docker.js +36 -23
  12. package/lib/parsers/iri.js +1 -2
  13. package/lib/server/server.js +164 -34
  14. package/lib/server/server.poku.js +232 -10
  15. package/lib/stages/postgen/annotator.js +281 -3
  16. package/lib/stages/postgen/postgen.js +4 -7
  17. package/lib/third-party/arborist/lib/diff.js +1 -1
  18. package/lib/third-party/arborist/lib/node.js +1 -1
  19. package/lib/third-party/arborist/lib/yarn-lock.js +1 -1
  20. package/package.json +22 -328
  21. package/types/bin/dependencies.d.ts.map +1 -1
  22. package/types/lib/cli/index.d.ts +39 -39
  23. package/types/lib/cli/index.d.ts.map +1 -1
  24. package/types/lib/evinser/evinser.d.ts +19 -19
  25. package/types/lib/evinser/evinser.d.ts.map +1 -1
  26. package/types/lib/evinser/swiftsem.d.ts +14 -14
  27. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  28. package/types/lib/helpers/cbomutils.d.ts +1 -1
  29. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  30. package/types/lib/helpers/db.d.ts +2 -2
  31. package/types/lib/helpers/db.d.ts.map +1 -1
  32. package/types/lib/helpers/display.d.ts +2 -2
  33. package/types/lib/helpers/display.d.ts.map +1 -1
  34. package/types/lib/helpers/envcontext.d.ts +14 -14
  35. package/types/lib/helpers/envcontext.d.ts.map +1 -1
  36. package/types/lib/helpers/logger.d.ts +1 -1
  37. package/types/lib/helpers/logger.d.ts.map +1 -1
  38. package/types/lib/helpers/protobom.d.ts +4 -2
  39. package/types/lib/helpers/protobom.d.ts.map +1 -1
  40. package/types/lib/helpers/utils.d.ts +103 -88
  41. package/types/lib/helpers/utils.d.ts.map +1 -1
  42. package/types/lib/helpers/validator.d.ts.map +1 -1
  43. package/types/lib/managers/binary.d.ts +2 -2
  44. package/types/lib/managers/binary.d.ts.map +1 -1
  45. package/types/lib/managers/docker.d.ts +2 -2
  46. package/types/lib/managers/docker.d.ts.map +1 -1
  47. package/types/lib/managers/oci.d.ts +1 -1
  48. package/types/lib/managers/oci.d.ts.map +1 -1
  49. package/types/lib/managers/piptree.d.ts +1 -1
  50. package/types/lib/managers/piptree.d.ts.map +1 -1
  51. package/types/lib/parsers/iri.d.ts +6 -6
  52. package/types/lib/parsers/iri.d.ts.map +1 -1
  53. package/types/lib/server/server.d.ts +14 -0
  54. package/types/lib/server/server.d.ts.map +1 -1
  55. package/types/lib/stages/postgen/annotator.d.ts +3 -3
  56. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  57. package/types/lib/stages/postgen/postgen.d.ts +5 -5
  58. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  59. package/types/lib/stages/pregen/pregen.d.ts +6 -6
  60. package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
  61. package/types/lib/third-party/arborist/lib/arborist/index.d.ts +4 -3
  62. package/types/lib/third-party/arborist/lib/arborist/index.d.ts.map +1 -1
  63. package/types/lib/third-party/arborist/lib/can-place-dep.d.ts +5 -5
  64. package/types/lib/third-party/arborist/lib/can-place-dep.d.ts.map +1 -1
  65. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts +4 -4
  66. package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts.map +1 -1
  67. package/types/lib/third-party/arborist/lib/diff.d.ts +3 -3
  68. package/types/lib/third-party/arborist/lib/diff.d.ts.map +1 -1
  69. package/types/lib/third-party/arborist/lib/edge.d.ts +2 -2
  70. package/types/lib/third-party/arborist/lib/edge.d.ts.map +1 -1
  71. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts +1 -1
  72. package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts.map +1 -1
  73. package/types/lib/third-party/arborist/lib/inventory.d.ts +3 -2
  74. package/types/lib/third-party/arborist/lib/inventory.d.ts.map +1 -1
  75. package/types/lib/third-party/arborist/lib/link.d.ts +10 -7
  76. package/types/lib/third-party/arborist/lib/link.d.ts.map +1 -1
  77. package/types/lib/third-party/arborist/lib/node.d.ts +8 -8
  78. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  79. package/types/lib/third-party/arborist/lib/optional-set.d.ts +1 -1
  80. package/types/lib/third-party/arborist/lib/optional-set.d.ts.map +1 -1
  81. package/types/lib/third-party/arborist/lib/override-set.d.ts +3 -3
  82. package/types/lib/third-party/arborist/lib/override-set.d.ts.map +1 -1
  83. package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts +1 -1
  84. package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts.map +1 -1
  85. package/types/lib/third-party/arborist/lib/place-dep.d.ts +3 -3
  86. package/types/lib/third-party/arborist/lib/place-dep.d.ts.map +1 -1
  87. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts +7 -7
  88. package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts.map +1 -1
  89. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts +1 -1
  90. package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts.map +1 -1
  91. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts +4 -3
  92. package/types/lib/third-party/arborist/lib/yarn-lock.d.ts.map +1 -1
  93. package/bin/dependencies.js +0 -131
  94. package/bin/licenses.js +0 -78
  95. package/lib/helpers/dependencies.poku.js +0 -11
  96. package/lib/helpers/licenses.poku.js +0 -11
  97. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts +0 -34
  98. package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts.map +0 -1
  99. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts +0 -24
  100. package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts.map +0 -1
  101. package/types/lib/third-party/arborist/lib/tracker.d.ts +0 -13
  102. package/types/lib/third-party/arborist/lib/tracker.d.ts.map +0 -1
@@ -66,6 +66,19 @@ app.use(
66
66
  );
67
67
  app.use(compression());
68
68
 
69
+ /**
70
+ * Return git allow protocol string from the environment variables.
71
+ *
72
+ * @returns {string} git allow protocol string
73
+ */
74
+ function getGitAllowProtocol() {
75
+ return (
76
+ process.env.GIT_ALLOW_PROTOCOL ||
77
+ process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL ||
78
+ (isSecureMode ? "https:ssh" : "https:git:ssh")
79
+ );
80
+ }
81
+
69
82
  /**
70
83
  * Checks the given hostname against the allowed list.
71
84
  *
@@ -152,13 +165,104 @@ export function isAllowedPath(p) {
152
165
  }
153
166
  return (process.env.CDXGEN_SERVER_ALLOWED_PATHS || "")
154
167
  .split(",")
155
- .some((ap) => p.startsWith(ap));
168
+ .filter(Boolean)
169
+ .some((ap) => {
170
+ const resolvedP = path.resolve(p);
171
+ const resolvedAp = path.resolve(ap);
172
+ const relativePath = path.relative(resolvedAp, resolvedP);
173
+ return (
174
+ relativePath === "" ||
175
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath))
176
+ );
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Determine if the file path could be a remote URL.
182
+ *
183
+ * @param {string} filePath The Git URL or local path
184
+ * @returns {Boolean} True if the file path is a remote URL. false otherwise.
185
+ */
186
+ export function maybeRemotePath(filePath) {
187
+ return /^[a-zA-Z0-9+.-]+:\/\//.test(filePath) || filePath.startsWith("git@");
188
+ }
189
+
190
+ /**
191
+ * Validates a given Git URL/Path against dangerous protocols and allowed hosts.
192
+ *
193
+ * @param {string} filePath The Git URL or local path
194
+ * @returns {Object|null} Error object if invalid, or null if valid
195
+ */
196
+ export function validateAndRejectGitSource(filePath) {
197
+ if (/^(ext|fd)::/i.test(filePath)) {
198
+ return {
199
+ status: 400,
200
+ error: "Invalid Protocol",
201
+ details: "The provided protocol is not allowed.",
202
+ };
203
+ }
204
+ if (maybeRemotePath(filePath)) {
205
+ let gitUrlObj;
206
+ try {
207
+ let urlToParse = filePath;
208
+ if (filePath.startsWith("git@") && !filePath.includes("://")) {
209
+ urlToParse = `ssh://${filePath.replace(":", "/")}`;
210
+ }
211
+ gitUrlObj = new URL(urlToParse);
212
+ } catch (_err) {
213
+ return {
214
+ status: 400,
215
+ error: "Invalid URL Format",
216
+ details: "The provided Git URL is malformed.",
217
+ };
218
+ }
219
+ const gitAllowProtocol = getGitAllowProtocol();
220
+ const allowedSchemes = gitAllowProtocol
221
+ .split(":")
222
+ .filter(Boolean)
223
+ .map((p) => `${p.toLowerCase()}:`);
224
+
225
+ if (
226
+ allowedSchemes.includes("ssh:") &&
227
+ !allowedSchemes.includes("git+ssh:")
228
+ ) {
229
+ allowedSchemes.push("git+ssh:");
230
+ }
231
+
232
+ if (!allowedSchemes.includes(gitUrlObj.protocol)) {
233
+ return {
234
+ status: 400,
235
+ error: "Protocol Not Allowed",
236
+ details: `The protocol '${gitUrlObj.protocol}' is not permitted by GIT_ALLOW_PROTOCOL.`,
237
+ };
238
+ }
239
+
240
+ if (gitUrlObj.href.includes("::")) {
241
+ return {
242
+ status: 400,
243
+ error: "Invalid URL Syntax",
244
+ details: "Git remote helper syntax (::) is not allowed.",
245
+ };
246
+ }
247
+
248
+ if (!isAllowedHost(gitUrlObj.hostname)) {
249
+ return {
250
+ status: 403,
251
+ error: "Host Not Allowed",
252
+ details: "The Git URL host is not allowed as per the allowlist.",
253
+ };
254
+ }
255
+ }
256
+
257
+ return null;
156
258
  }
157
259
 
158
260
  function gitClone(repoUrl, branch = null) {
159
- const tempDir = fs.mkdtempSync(
160
- path.join(getTmpDir(), path.basename(repoUrl)),
161
- );
261
+ let baseDirName = path.basename(repoUrl);
262
+ if (!/^[a-zA-Z0-9_-]+$/.test(baseDirName)) {
263
+ baseDirName = "repo-";
264
+ }
265
+ const tempDir = fs.mkdtempSync(path.join(getTmpDir(), baseDirName));
162
266
 
163
267
  const gitArgs = [
164
268
  "-c",
@@ -170,16 +274,30 @@ function gitClone(repoUrl, branch = null) {
170
274
  tempDir,
171
275
  ];
172
276
  if (branch) {
173
- const cloneIndex = gitArgs.indexOf("clone");
174
- gitArgs.splice(cloneIndex + 1, 0, "--branch", branch);
277
+ const firstBranchStr = Array.isArray(branch) ? branch[0] : String(branch);
278
+ if (firstBranchStr.startsWith("-")) {
279
+ console.log(
280
+ `Skipping branch clone: invalid branch name ${firstBranchStr}`,
281
+ );
282
+ } else {
283
+ const cloneIndex = gitArgs.indexOf("clone");
284
+ gitArgs.splice(cloneIndex + 1, 0, "--branch", firstBranchStr);
285
+ }
175
286
  }
176
287
  console.log(
177
288
  `Cloning Repo${branch ? ` with branch ${branch}` : ""} to ${tempDir}`,
178
289
  );
290
+ const gitAllowProtocol = getGitAllowProtocol();
179
291
  // See issue #1956
180
292
  const env = isSecureMode
181
- ? { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_NOGLOBAL: "1" }
182
- : { ...process.env };
293
+ ? {
294
+ ...process.env,
295
+ GIT_CONFIG_NOSYSTEM: "1",
296
+ GIT_CONFIG_NOGLOBAL: "1",
297
+ GIT_ALLOW_PROTOCOL: gitAllowProtocol,
298
+ }
299
+ : { ...process.env, GIT_ALLOW_PROTOCOL: gitAllowProtocol };
300
+
183
301
  const result = safeSpawnSync("git", gitArgs, {
184
302
  shell: false,
185
303
  env,
@@ -352,7 +470,21 @@ const start = (options) => {
352
470
  }
353
471
  if (!process.env.CDXGEN_SERVER_ALLOWED_HOSTS) {
354
472
  console.log(
355
- "No allowlist for hosts has been specified. This is a security risk that could expose the system to SSRF vulnerabilities!",
473
+ "No allowlist for git hosts has been specified. This is a security risk that could expose the system to SSRF vulnerabilities!",
474
+ );
475
+ if (isSecureMode) {
476
+ process.exit(1);
477
+ }
478
+ }
479
+ if (isSecureMode && !process.env.CDXGEN_SERVER_ALLOWED_PATHS) {
480
+ console.log(
481
+ "No allowlist for paths has been specified. This is a security risk that could expose the filesystem and internal secrets!",
482
+ );
483
+ process.exit(1);
484
+ }
485
+ if (/(ext|fd):/i.test(getGitAllowProtocol())) {
486
+ console.log(
487
+ "The Git protocols 'ext' and 'fd' are known to be problematic. Allowing those is a security risk that could expose the system to RCE vulnerabilities!",
356
488
  );
357
489
  if (isSecureMode) {
358
490
  process.exit(1);
@@ -406,22 +538,24 @@ const start = (options) => {
406
538
  }),
407
539
  );
408
540
  }
409
- let srcDir = filePath;
410
- if (filePath.startsWith("http") || filePath.startsWith("git")) {
411
- // Validate the hostnames
412
- const gitUrlObj = new URL(filePath);
413
- if (!isAllowedHost(gitUrlObj.hostname)) {
414
- res.writeHead(403, { "Content-Type": "application/json" });
415
- return res.end(
416
- JSON.stringify({
417
- error: "Host Not Allowed",
418
- details: "The Git URL host is not allowed as per the allowlist.",
419
- }),
420
- );
421
- }
541
+ const validationError = validateAndRejectGitSource(filePath);
542
+ if (validationError) {
543
+ res.writeHead(validationError.status, {
544
+ "Content-Type": "application/json",
545
+ });
546
+ return res.end(
547
+ JSON.stringify({
548
+ error: validationError.error,
549
+ details: validationError.details,
550
+ }),
551
+ );
552
+ }
553
+ let srcDir;
554
+ if (maybeRemotePath(filePath)) {
422
555
  srcDir = gitClone(filePath, reqOptions.gitBranch);
423
556
  cleanup = true;
424
557
  } else {
558
+ srcDir = filePath;
425
559
  if (
426
560
  !isAllowedPath(path.resolve(srcDir)) ||
427
561
  (isWin && !isAllowedWinPath(srcDir))
@@ -436,18 +570,13 @@ const start = (options) => {
436
570
  }
437
571
  }
438
572
  if (srcDir !== path.resolve(srcDir)) {
439
- console.log(
440
- `Invoke the API with an absolute path '${path.resolve(srcDir)}' to reduce security risks.`,
573
+ res.writeHead(500, { "Content-Type": "application/json" });
574
+ return res.end(
575
+ JSON.stringify({
576
+ error: "Absolute path needed",
577
+ details: "Relative paths are not supported in server mode.",
578
+ }),
441
579
  );
442
- if (isSecureMode) {
443
- res.writeHead(500, { "Content-Type": "application/json" });
444
- return res.end(
445
- JSON.stringify({
446
- error: "Absolute path needed",
447
- details: "Relative paths are not supported in secure mode.",
448
- }),
449
- );
450
- }
451
580
  }
452
581
  console.log("Generating SBOM for", srcDir);
453
582
  let bomNSData = (await createBom(srcDir, reqOptions)) || {};
@@ -498,10 +627,11 @@ const start = (options) => {
498
627
  }
499
628
  }
500
629
  res.end("\n");
501
- if (cleanup && srcDir && srcDir.startsWith(getTmpDir()) && fs.rmSync) {
630
+ if (cleanup && srcDir?.startsWith(getTmpDir()) && fs.rmSync) {
502
631
  console.log(`Cleaning up ${srcDir}`);
503
632
  fs.rmSync(srcDir, { recursive: true, force: true });
504
633
  }
505
634
  });
506
635
  };
636
+
507
637
  export { configureServer, start };
@@ -8,6 +8,7 @@ import {
8
8
  isAllowedWinPath,
9
9
  parseQueryString,
10
10
  parseValue,
11
+ validateAndRejectGitSource,
11
12
  } from "./server.js";
12
13
 
13
14
  it("parseValue tests", () => {
@@ -129,28 +130,73 @@ describe("isAllowedPath()", () => {
129
130
  });
130
131
 
131
132
  afterEach(() => {
132
- process.env.CDXGEN_SERVER_ALLOWED_PATHS = originalPaths;
133
+ if (originalPaths === undefined) {
134
+ delete process.env.CDXGEN_SERVER_ALLOWED_PATHS;
135
+ } else {
136
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = originalPaths;
137
+ }
138
+ });
139
+
140
+ it("returns false for non-string inputs", () => {
141
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api";
142
+ assert.strictEqual(isAllowedPath(null), false);
143
+ assert.strictEqual(isAllowedPath(123), false);
144
+ assert.strictEqual(isAllowedPath({}), false);
145
+ assert.strictEqual(isAllowedPath(undefined), false);
133
146
  });
134
147
 
135
148
  it("returns true if CDXGEN_SERVER_ALLOWED_PATHS is not set", () => {
136
149
  delete process.env.CDXGEN_SERVER_ALLOWED_PATHS;
137
- assert.deepStrictEqual(isAllowedPath("/any/path"), true);
150
+ assert.strictEqual(isAllowedPath("/any/path"), true);
138
151
  });
139
152
 
140
- it("returns true for paths that start with an allowed prefix", () => {
153
+ it("treats an empty-string env var as unset (returns true)", () => {
154
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "";
155
+ assert.strictEqual(isAllowedPath("/anything"), true);
156
+ });
157
+
158
+ it("returns true for exact directory matches", () => {
141
159
  process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api,/public";
142
- assert.deepStrictEqual(isAllowedPath("/api/resource"), true);
143
- assert.deepStrictEqual(isAllowedPath("/public/index.html"), true);
160
+ assert.strictEqual(isAllowedPath("/api"), true);
161
+ assert.strictEqual(isAllowedPath("/public"), true);
144
162
  });
145
163
 
146
- it("returns false for paths that do not match any prefix", () => {
164
+ it("returns true for files safely nested inside allowed directories", () => {
147
165
  process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api,/public";
148
- assert.deepStrictEqual(isAllowedPath("/private/data"), false);
166
+ assert.strictEqual(isAllowedPath("/api/resource"), true);
167
+ assert.strictEqual(isAllowedPath("/public/index.html"), true);
168
+ assert.strictEqual(isAllowedPath("/public/assets/css/main.css"), true);
149
169
  });
150
170
 
151
- it("treats an empty-string env var as unset (returns true)", () => {
152
- process.env.CDXGEN_SERVER_ALLOWED_PATHS = "";
153
- assert.deepStrictEqual(isAllowedPath("/anything"), true);
171
+ it("returns false for completely unrelated paths", () => {
172
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api,/public";
173
+ assert.strictEqual(isAllowedPath("/private/data"), false);
174
+ assert.strictEqual(isAllowedPath("/etc/passwd"), false);
175
+ });
176
+
177
+ it("prevents directory prefix bypass (e.g., /var/www vs /var/www-secret)", () => {
178
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api,/var/www";
179
+ assert.strictEqual(isAllowedPath("/api-secret/data"), false);
180
+ assert.strictEqual(isAllowedPath("/api-secret"), false);
181
+ assert.strictEqual(isAllowedPath("/var/www-backup"), false);
182
+ });
183
+
184
+ it("prevents path traversal attacks using ../", () => {
185
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api";
186
+ assert.strictEqual(isAllowedPath("/api/../private"), false);
187
+ assert.strictEqual(isAllowedPath("/api/../../etc/passwd"), false);
188
+ });
189
+
190
+ it("allows paths that contain ../ but safely resolve inside the allowed directory", () => {
191
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api";
192
+ assert.strictEqual(isAllowedPath("/api/resource/../data"), true);
193
+ });
194
+
195
+ it("gracefully handles comma-separated lists with empty segments", () => {
196
+ process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api,,/public,";
197
+ assert.strictEqual(isAllowedPath("/api/resource"), true);
198
+ assert.strictEqual(isAllowedPath("/public/index.html"), true);
199
+ assert.strictEqual(isAllowedPath("/private/data"), false);
154
200
  });
155
201
  });
156
202
 
@@ -385,3 +431,179 @@ describe("getQueryParams", () => {
385
431
  });
386
432
  });
387
433
  });
434
+ describe("validateGitSource() tests", () => {
435
+ let originalGitAllow;
436
+ let originalAllowedHosts;
437
+
438
+ beforeEach(() => {
439
+ originalGitAllow = process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL;
440
+ originalAllowedHosts = process.env.CDXGEN_SERVER_ALLOWED_HOSTS;
441
+ delete process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL;
442
+ delete process.env.CDXGEN_SERVER_ALLOWED_HOSTS;
443
+ });
444
+
445
+ afterEach(() => {
446
+ if (originalGitAllow)
447
+ process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL = originalGitAllow;
448
+ if (originalAllowedHosts)
449
+ process.env.CDXGEN_SERVER_ALLOWED_HOSTS = originalAllowedHosts;
450
+ });
451
+
452
+ it("should reject ext:: and fd:: outright", () => {
453
+ assert.deepStrictEqual(
454
+ validateAndRejectGitSource("ext::sh -c id").error,
455
+ "Invalid Protocol",
456
+ );
457
+ assert.deepStrictEqual(
458
+ validateAndRejectGitSource("fd::123").error,
459
+ "Invalid Protocol",
460
+ );
461
+ assert.deepStrictEqual(
462
+ validateAndRejectGitSource("EXT::sh -c id").error,
463
+ "Invalid Protocol",
464
+ );
465
+ });
466
+
467
+ it("should allow standard local paths to bypass validation", () => {
468
+ assert.deepStrictEqual(validateAndRejectGitSource("/tmp/local-path"), null);
469
+ assert.deepStrictEqual(
470
+ validateAndRejectGitSource("C:\\Users\\local"),
471
+ null,
472
+ );
473
+ });
474
+
475
+ it("should handle ssh git@ format gracefully", () => {
476
+ assert.deepStrictEqual(
477
+ validateAndRejectGitSource("git@github.com:foo/bar.git"),
478
+ null,
479
+ );
480
+ });
481
+
482
+ it("should reject malformed git URLs", () => {
483
+ // invalid URL format (can't parse via node's new URL object)
484
+ assert.deepStrictEqual(
485
+ validateAndRejectGitSource("http://[:::1]/bad-ipv6").error,
486
+ "Invalid URL Format",
487
+ );
488
+ });
489
+
490
+ it("should enforce GIT_ALLOW_PROTOCOL default schemes", () => {
491
+ assert.deepStrictEqual(
492
+ validateAndRejectGitSource("https://github.com/repo"),
493
+ null,
494
+ );
495
+ assert.deepStrictEqual(
496
+ validateAndRejectGitSource("http://github.com/repo"),
497
+ {
498
+ status: 400,
499
+ error: "Protocol Not Allowed",
500
+ details: "The protocol 'http:' is not permitted by GIT_ALLOW_PROTOCOL.",
501
+ },
502
+ );
503
+ assert.deepStrictEqual(
504
+ validateAndRejectGitSource("git://github.com/repo"),
505
+ null,
506
+ );
507
+ assert.deepStrictEqual(
508
+ validateAndRejectGitSource("ssh://github.com/repo"),
509
+ null,
510
+ );
511
+ assert.deepStrictEqual(
512
+ validateAndRejectGitSource("git+ssh://github.com/repo"),
513
+ null,
514
+ );
515
+
516
+ // ftp is not allowed by default
517
+ const res = validateAndRejectGitSource("ftp://github.com/repo");
518
+ assert.deepStrictEqual(res.error, "Protocol Not Allowed");
519
+ assert.deepStrictEqual(
520
+ res.details,
521
+ "The protocol 'ftp:' is not permitted by GIT_ALLOW_PROTOCOL.",
522
+ );
523
+ });
524
+
525
+ it("should reject protocol smuggling techniques", () => {
526
+ assert.deepStrictEqual(
527
+ validateAndRejectGitSource("git+ext://github.com/repo").error,
528
+ "Protocol Not Allowed",
529
+ );
530
+ assert.deepStrictEqual(
531
+ validateAndRejectGitSource("http+ext://github.com/repo").error,
532
+ "Protocol Not Allowed",
533
+ );
534
+ });
535
+
536
+ it("should respect custom CDXGEN_SERVER_GIT_ALLOW_PROTOCOL configs", () => {
537
+ process.env.CDXGEN_SERVER_GIT_ALLOW_PROTOCOL = "https:git";
538
+ assert.deepStrictEqual(
539
+ validateAndRejectGitSource("https://github.com/repo"),
540
+ null,
541
+ );
542
+ assert.deepStrictEqual(
543
+ validateAndRejectGitSource("git://github.com/repo"),
544
+ null,
545
+ );
546
+
547
+ // http is no longer allowed
548
+ const res = validateAndRejectGitSource("http://github.com/repo");
549
+ assert.deepStrictEqual(res.error, "Protocol Not Allowed");
550
+ assert.deepStrictEqual(
551
+ res.details,
552
+ "The protocol 'http:' is not permitted by GIT_ALLOW_PROTOCOL.",
553
+ );
554
+ });
555
+
556
+ it("should reject remote helper syntax (::) inside valid schemes", () => {
557
+ assert.deepStrictEqual(
558
+ validateAndRejectGitSource("https://github.com/ext::sh -c id").error,
559
+ "Invalid URL Syntax",
560
+ );
561
+ assert.deepStrictEqual(
562
+ validateAndRejectGitSource("git://foo::bar/repo").error,
563
+ "Invalid URL Format",
564
+ );
565
+ });
566
+
567
+ it("should validate allowed hosts", () => {
568
+ process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "github.com,gitlab.com";
569
+ assert.deepStrictEqual(
570
+ validateAndRejectGitSource("https://github.com/repo"),
571
+ null,
572
+ );
573
+
574
+ const res = validateAndRejectGitSource("https://evil.com/repo");
575
+ assert.deepStrictEqual(res.error, "Host Not Allowed");
576
+ assert.deepStrictEqual(res.status, 403);
577
+ });
578
+ });
579
+ it("should correctly normalize and validate various git@ (SCP-like) formats", () => {
580
+ assert.deepStrictEqual(
581
+ validateAndRejectGitSource("git@gitlab.com:group/project.git"),
582
+ null,
583
+ );
584
+ assert.deepStrictEqual(
585
+ validateAndRejectGitSource("git@bitbucket.org:workspace/repo:name.git"),
586
+ null,
587
+ );
588
+ assert.deepStrictEqual(
589
+ validateAndRejectGitSource("git@github.com/user/repo.git"),
590
+ null,
591
+ );
592
+ assert.deepStrictEqual(
593
+ validateAndRejectGitSource("ssh://git@github.com/user/repo.git"),
594
+ null,
595
+ );
596
+ process.env.CDXGEN_SERVER_ALLOWED_HOSTS = "github.com,bitbucket.org";
597
+ assert.deepStrictEqual(
598
+ validateAndRejectGitSource("git@github.com:user/repo.git"),
599
+ null,
600
+ );
601
+ assert.deepStrictEqual(
602
+ validateAndRejectGitSource("git@bitbucket.org:workspace/repo.git"),
603
+ null,
604
+ );
605
+ const deniedRes = validateAndRejectGitSource("git@evil.com:foo/bar.git");
606
+ assert.deepStrictEqual(deniedRes.status, 403);
607
+ assert.deepStrictEqual(deniedRes.error, "Host Not Allowed");
608
+ delete process.env.CDXGEN_SERVER_ALLOWED_HOSTS;
609
+ });