@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.
- package/README.md +11 -9
- package/bin/cdxgen.js +1 -1
- package/lib/cli/index.js +9 -5
- package/lib/evinser/evinser.js +2 -8
- package/lib/helpers/display.js +1 -1
- package/lib/helpers/envcontext.js +10 -2
- package/lib/helpers/utils.js +462 -86
- package/lib/helpers/utils.poku.js +179 -2
- package/lib/helpers/validator.js +8 -5
- package/lib/managers/docker.getConnection.poku.js +61 -0
- package/lib/managers/docker.js +36 -23
- package/lib/parsers/iri.js +1 -2
- package/lib/server/server.js +164 -34
- package/lib/server/server.poku.js +232 -10
- package/lib/stages/postgen/annotator.js +281 -3
- package/lib/stages/postgen/postgen.js +4 -7
- package/lib/third-party/arborist/lib/diff.js +1 -1
- package/lib/third-party/arborist/lib/node.js +1 -1
- package/lib/third-party/arborist/lib/yarn-lock.js +1 -1
- package/package.json +22 -328
- package/types/bin/dependencies.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +39 -39
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +19 -19
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +14 -14
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +1 -1
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/db.d.ts +2 -2
- package/types/lib/helpers/db.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +2 -2
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +14 -14
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/logger.d.ts +1 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +4 -2
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +103 -88
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/validator.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +2 -2
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +2 -2
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +1 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/iri.d.ts +6 -6
- package/types/lib/parsers/iri.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +14 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts +3 -3
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +5 -5
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +6 -6
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/arborist/index.d.ts +4 -3
- package/types/lib/third-party/arborist/lib/arborist/index.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/can-place-dep.d.ts +5 -5
- package/types/lib/third-party/arborist/lib/can-place-dep.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts +4 -4
- package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/diff.d.ts +3 -3
- package/types/lib/third-party/arborist/lib/diff.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/edge.d.ts +2 -2
- package/types/lib/third-party/arborist/lib/edge.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts +1 -1
- package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/inventory.d.ts +3 -2
- package/types/lib/third-party/arborist/lib/inventory.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/link.d.ts +10 -7
- package/types/lib/third-party/arborist/lib/link.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/node.d.ts +8 -8
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/optional-set.d.ts +1 -1
- package/types/lib/third-party/arborist/lib/optional-set.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/override-set.d.ts +3 -3
- package/types/lib/third-party/arborist/lib/override-set.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts +1 -1
- package/types/lib/third-party/arborist/lib/peer-entry-sets.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/place-dep.d.ts +3 -3
- package/types/lib/third-party/arborist/lib/place-dep.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts +7 -7
- package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts +1 -1
- package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts.map +1 -1
- package/types/lib/third-party/arborist/lib/yarn-lock.d.ts +4 -3
- package/types/lib/third-party/arborist/lib/yarn-lock.d.ts.map +1 -1
- package/bin/dependencies.js +0 -131
- package/bin/licenses.js +0 -78
- package/lib/helpers/dependencies.poku.js +0 -11
- package/lib/helpers/licenses.poku.js +0 -11
- package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts +0 -34
- package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts.map +0 -1
- package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts +0 -24
- package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts.map +0 -1
- package/types/lib/third-party/arborist/lib/tracker.d.ts +0 -13
- package/types/lib/third-party/arborist/lib/tracker.d.ts.map +0 -1
package/lib/server/server.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
174
|
-
|
|
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
|
-
? {
|
|
182
|
-
|
|
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
|
-
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
150
|
+
assert.strictEqual(isAllowedPath("/any/path"), true);
|
|
138
151
|
});
|
|
139
152
|
|
|
140
|
-
it("
|
|
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.
|
|
143
|
-
assert.
|
|
160
|
+
assert.strictEqual(isAllowedPath("/api"), true);
|
|
161
|
+
assert.strictEqual(isAllowedPath("/public"), true);
|
|
144
162
|
});
|
|
145
163
|
|
|
146
|
-
it("returns
|
|
164
|
+
it("returns true for files safely nested inside allowed directories", () => {
|
|
147
165
|
process.env.CDXGEN_SERVER_ALLOWED_PATHS = "/api,/public";
|
|
148
|
-
assert.
|
|
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("
|
|
152
|
-
process.env.CDXGEN_SERVER_ALLOWED_PATHS = "";
|
|
153
|
-
assert.
|
|
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
|
+
});
|