mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 21:44:59 -05:00
Raise nodemailer, body-parser, qs, uuid and postcss-selector-parser in the bundle.
A Grype scan of the image found advisories in packages that Meteor's own packages carry in programs/server/npm/node_modules/meteor/<package>, which package.json cannot reach. nodemailer 8.0.11 -> 10.3.1: meteor/email only calls createTransport, the 'stream' plugin hook, nodemailer/lib/well-known and nodemailer/lib/mail-composer, and all four work in 10.3.1 (checked with the bundle's own package specifiers, through its exports map). body-parser 1.20.6/1.20.8 -> 1.20.9 and 2.3.0 -> 2.4.0: a minimum may now be "name@MAJOR", so each major is raised inside itself. qs -> 6.16.1, postcss-selector-parser 6.1.4 -> 6.1.5. uuid 8.3.2 and 9.0.1 -> 11.1.1: 11 moved its entry point, which is why it was refused before. The bumper now writes the recorded dist/index.js back as an ESM re-export of dist/esm/index.js, which require() loads and which leaves uuid's own ESM and CJS entry points as they are. glob 14 and minimatch 11 are already in package.json. Run against a copy of the published image's bundle, the bumper raises all of these and every raised copy loads. Thanks to the Grype scan reporter and xet7 !
This commit is contained in:
1 parent
3213d7fa1d
commit
dad03959d9
3 files changed
+102
-55
No files matched your search
@@ -155,14 +155,20 @@ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wekan-bundle-bump-'));
|
||||
const bumped = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const [name, minimum] of Object.entries(manifest.minimums)) {
|
||||
const outdated = copiesOf(name).filter(c => compare(c.version, minimum) < 0);
|
||||
for (const [key, minimum] of Object.entries(manifest.minimums)) {
|
||||
// "body-parser@2" limits the minimum to copies of that major; "@babel/runtime"
|
||||
// is a scoped name, not a major, so only an @ after the first character counts.
|
||||
const at = key.lastIndexOf('@');
|
||||
const name = at > 0 ? key.slice(0, at) : key;
|
||||
const major = at > 0 ? key.slice(at + 1) : null;
|
||||
const outdated = copiesOf(name).filter(c =>
|
||||
(major === null || c.version.split('.')[0] === major) && compare(c.version, minimum) < 0);
|
||||
if (!outdated.length) {
|
||||
skipped.push(name);
|
||||
skipped.push(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
const prefix = path.join(tmpRoot, name.replace('/', '+'));
|
||||
const prefix = path.join(tmpRoot, key.replace(/[\/@]/g, '+') + '-' + minimum);
|
||||
if (!dryRun) {
|
||||
fs.mkdirSync(prefix, { recursive: true });
|
||||
// A prefix with no package.json is what we want: npm installs the one
|
||||
@@ -180,6 +186,15 @@ for (const [name, minimum] of Object.entries(manifest.minimums)) {
|
||||
|
||||
fs.rmSync(copy.dir, { recursive: true, force: true });
|
||||
fs.cpSync(path.join(prefix, 'node_modules', name), copy.dir, { recursive: true });
|
||||
// The entry file the bundle recorded for the OLD version, written back so it
|
||||
// resolves again (uuid 11 moved dist/index.js to dist/cjs/index.js).
|
||||
for (const [rel, body] of Object.entries((manifest.shims || {})[name] || {})) {
|
||||
const file = path.join(copy.dir, rel);
|
||||
if (fs.existsSync(file)) continue;
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, body);
|
||||
console.log(` + ${rel} (shim for the entry point ${name} ${copy.version} was built against)`);
|
||||
}
|
||||
|
||||
// Whatever the new version needs that this node_modules does not have. An
|
||||
// existing sibling is left as it is - it is some other package's choice.
|
||||
|
||||
@@ -7,10 +7,20 @@
|
||||
"be raised is the bundle, after `meteor build` has produced it.",
|
||||
"",
|
||||
"Every entry is a version that FIXES a published advisory in the version the",
|
||||
"bundle would otherwise ship, and stays INSIDE THE SAME MAJOR. That is not",
|
||||
"caution for its own sake: a Meteor bundle records the file a package resolves",
|
||||
"to when it is built, so a major that moves its entry point cannot be swapped",
|
||||
"in afterwards - see uuid under notFixable, which is what taught us that.",
|
||||
"bundle would otherwise ship. A minimum stays inside the same major WHEN a fix",
|
||||
"exists there. When the fix is only in a newer major it is still taken, but only",
|
||||
"after the new major was run the way the Meteor package uses it: nodemailer 10",
|
||||
"against the calls meteor/email makes, uuid 11 through a shim (see \"shims\").",
|
||||
"",
|
||||
"A key may be \"name@MAJOR\" (body-parser@2): the minimum then applies only to",
|
||||
"copies of that major, so body-parser 1.x and 2.x are each raised inside their",
|
||||
"own major and neither is dragged across it.",
|
||||
"",
|
||||
"A bundle records the file a package resolves to when it is built, so a major",
|
||||
"that moved its entry point cannot just be swapped in: uuid 11 moved it from",
|
||||
"dist/index.js to dist/cjs/index.js and the server died on boot (\"Cannot find",
|
||||
"module .../uuid/dist/index.js\"). \"shims\" writes the old entry file back,",
|
||||
"re-exporting the new one.",
|
||||
"",
|
||||
"Applied by releases/bump-bundle-npm-deps.mjs, pinned by",
|
||||
"tests/bundleNpmSecurityBumps.test.cjs. A package NOT listed here is a package",
|
||||
@@ -21,7 +31,8 @@
|
||||
"minimums": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"baseline-browser-mapping": "2.11.0",
|
||||
"body-parser": "1.20.6",
|
||||
"body-parser": "1.20.9",
|
||||
"body-parser@2": "2.4.0",
|
||||
"browserslist": "4.28.7",
|
||||
"colord": "2.9.4",
|
||||
"compression": "1.8.2",
|
||||
@@ -30,43 +41,30 @@
|
||||
"lodash": "4.18.1",
|
||||
"lodash.template": "4.18.1",
|
||||
"nanoid": "3.3.18",
|
||||
"nodemailer": "8.0.11",
|
||||
"nodemailer": "10.3.1",
|
||||
"on-headers": "1.1.0",
|
||||
"openpgp": "5.11.3",
|
||||
"picomatch": "4.0.5",
|
||||
"postcss": "8.5.26",
|
||||
"qs": "6.16.0",
|
||||
"postcss-selector-parser": "6.1.5",
|
||||
"qs": "6.16.1",
|
||||
"source-map-js": "1.2.2",
|
||||
"svgo": "2.8.4",
|
||||
"tmp": "0.2.7",
|
||||
"underscore": "1.13.8"
|
||||
"underscore": "1.13.8",
|
||||
"uuid": "11.1.1"
|
||||
},
|
||||
"notFixable": {
|
||||
"nodemailer": [
|
||||
"8.0.11 is the newest 8.x. CVE-2026-82659 (fixed in 9.0.1), GHSA-2x7j-588g-ccc2,",
|
||||
"GHSA-cc9r-2j5m-2m83 and GHSA-wmmp-3585-3rmp (9.1.0), GHSA-8m3c-c648-2xjj (9.1.1),",
|
||||
"GHSA-6vj9-mwq6-2f5v and GHSA-8vvx-rff5-p5rq (10.0.2) and GHSA-v53p-9fqp-m79j",
|
||||
"(10.0.6) are fixed only in majors the Meteor email package is not written",
|
||||
"against. They move when meteor/email moves."
|
||||
],
|
||||
"postcss-selector-parser": [
|
||||
"CVE-2026-104844 in the 6.1.4 meteor/minifier-css brings (through cssnano) is",
|
||||
"fixed only in 7.1.6, a new major its dependents are not written against.",
|
||||
"minifier-css runs at build time; it moves when Meteor's minifier-css moves."
|
||||
],
|
||||
"uuid": [
|
||||
"CVE-2026-41907 affects the 8.3.2 in meteor/ddp-server and",
|
||||
"meteor/inter-process-messaging and the 9.0.1 ldapts brings to wekan-ldap,",
|
||||
"and the lowest fixed release is 11.1.1 - a major that MOVED its entry",
|
||||
"point from dist/index.js to dist/cjs/index.js. The bundle resolves the old",
|
||||
"path, so putting 11.1.1 in its place stops the server before it starts:",
|
||||
"",
|
||||
" error on boot.js Error: Cannot find module",
|
||||
" \"/node_modules/meteor/inter-process-messaging/node_modules/uuid/dist/index.js\"",
|
||||
"",
|
||||
"It was tried, it failed exactly there, and it is left at the version the",
|
||||
"packages were built against. It moves when those packages move: Meteor's",
|
||||
"for ddp-server and inter-process-messaging, ldapts' for wekan-ldap."
|
||||
"6.1.5 fixes GHSA-rj75-hqrm-r3gf and is taken. CVE-2026-104844 in the copy",
|
||||
"meteor/minifier-css brings (through cssnano) is fixed only in 7.1.6, a new major",
|
||||
"its dependents are not written against. minifier-css runs at build time; it",
|
||||
"moves when Meteor's minifier-css moves."
|
||||
]
|
||||
},
|
||||
"shims": {
|
||||
"uuid": {
|
||||
"dist/index.js": "export * from './esm/index.js';\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +41,9 @@ console.log('bundleNpmSecurityBumps:');
|
||||
|
||||
test('the manifest lists the packages the scan found, at fixed versions', () => {
|
||||
const expected = {
|
||||
'nodemailer': '8.0', 'openpgp': '5.11', 'svgo': '2.8.4', 'postcss': '8.5',
|
||||
'nanoid': '3.3', 'lodash': '4.18', 'qs': '6.16', 'body-parser': '1.20',
|
||||
'nodemailer': '10.3', 'openpgp': '5.11', 'svgo': '2.8.4', 'postcss': '8.5',
|
||||
'nanoid': '3.3', 'lodash': '4.18', 'qs': '6.16.1', 'body-parser': '1.20.9', 'body-parser@2': '2.4',
|
||||
'uuid': '11.1.1', 'postcss-selector-parser': '6.1.5',
|
||||
'cookie': '0.7', 'on-headers': '1.1', 'tmp': '0.2', 'diff': '3.5',
|
||||
'@babel/runtime': '7.', 'underscore': '1.13.8',
|
||||
// Artifact Hub's Trivy report of the v12.38 image: same-major fixes.
|
||||
@@ -57,25 +58,58 @@ test('the manifest lists the packages the scan found, at fixed versions', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('uuid is NOT bumped, and says why (negative)', () => {
|
||||
// The one that was tried and reverted. If somebody adds it back, the bundle
|
||||
// stops booting - so the reason lives where they would look.
|
||||
assert.ok(!('uuid' in manifest.minimums), 'uuid must not be in minimums');
|
||||
const reason = (manifest.notFixable.uuid || []).join(' ');
|
||||
assert.ok(/Cannot find module/.test(reason) && /dist\/cjs\/index\.js/.test(reason),
|
||||
'and notFixable carries the boot error it caused');
|
||||
test('uuid is bumped to 11.1.1 and the entry point the bundle recorded is written back', () => {
|
||||
// uuid 11 moved its entry from dist/index.js to dist/cjs/index.js, and a bundle
|
||||
// that recorded dist/index.js died on boot ("Cannot find module
|
||||
// .../uuid/dist/index.js"). The fix is a shim at the old path, not a refusal.
|
||||
assert.strictEqual(manifest.minimums.uuid, '11.1.1');
|
||||
const shim = (manifest.shims || {}).uuid || {};
|
||||
assert.strictEqual(shim['dist/index.js'], "export * from './esm/index.js';\n",
|
||||
'dist/index.js re-exports the ESM build (uuid is "type": "module"; a CJS shim would be parsed as ESM)');
|
||||
assert.ok(/manifest\.shims/.test(bumper) && /writeFileSync\(file, body\)/.test(bumper),
|
||||
'and the bumper writes it after every replacement');
|
||||
// negative: nothing is described as unfixable that was fixed
|
||||
for (const name of ['uuid', 'nodemailer']) {
|
||||
assert.ok(!(name in manifest.notFixable), `${name} must not stay in notFixable`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a fix that needs a new major is recorded in notFixable, never in minimums (negative)', () => {
|
||||
for (const name of ['nodemailer', 'postcss-selector-parser', 'uuid']) {
|
||||
const reason = (manifest.notFixable[name] || []).join(' ');
|
||||
assert.ok(/major/.test(reason), `${name} says the fix is a new major`);
|
||||
}
|
||||
// nodemailer stays at the newest 8.x; the 9.x/10.x fixes are named, not taken.
|
||||
assert.ok(manifest.minimums.nodemailer.startsWith('8.'));
|
||||
assert.ok(!('postcss-selector-parser' in manifest.minimums));
|
||||
// lodash.template has had a fixed release (4.18.x) since 2026-04; it is no
|
||||
// longer described as unfixable.
|
||||
test('body-parser 1.x and 2.x are each raised inside their own major', () => {
|
||||
assert.strictEqual(manifest.minimums['body-parser'], '1.20.9');
|
||||
assert.strictEqual(manifest.minimums['body-parser@2'], '2.4.0');
|
||||
const fs2 = require('fs'), os = require('os'), cp = require('child_process');
|
||||
const bundle = fs2.mkdtempSync(path.join(process.env.TMPDIR || os.tmpdir(), 'bump-test-'));
|
||||
const put = (dir, version) => {
|
||||
const d = path.join(bundle, 'programs/server/npm/node_modules', dir);
|
||||
fs2.mkdirSync(d, { recursive: true });
|
||||
fs2.writeFileSync(path.join(d, 'package.json'), JSON.stringify({ name: 'x', version }));
|
||||
};
|
||||
put('meteor/a/node_modules/body-parser', '1.20.6');
|
||||
put('meteor/b/node_modules/body-parser', '2.3.0');
|
||||
put('meteor/c/node_modules/body-parser', '2.4.0');
|
||||
put('meteor/d/node_modules/body-parser', '1.20.9');
|
||||
put('meteor/e/node_modules/uuid', '8.3.2');
|
||||
put('meteor/f/node_modules/uuid', '14.0.2');
|
||||
put('meteor/g/node_modules/nodemailer', '8.0.11');
|
||||
const out = cp.execFileSync(process.execPath,
|
||||
[path.join(repoRoot, 'releases/bump-bundle-npm-deps.mjs'), bundle, '--dry-run'], { encoding: 'utf8' });
|
||||
fs2.rmSync(bundle, { recursive: true, force: true });
|
||||
const lines = out.split('\n').filter(l => /->/.test(l));
|
||||
const has = re => lines.some(l => re.test(l));
|
||||
assert.ok(has(/body-parser 1\.20\.6 -> 1\.20\.9/), '1.x below 1.20.9 is raised to 1.20.9');
|
||||
assert.ok(has(/body-parser 2\.3\.0 -> 2\.4\.0/), '2.3.0 is raised to 2.4.0');
|
||||
// negative: neither crosses into the other major, and fixed copies are left alone
|
||||
assert.ok(!has(/body-parser 1\.\d+\.\d+ -> 2\./) && !has(/body-parser 2\.\d+\.\d+ -> 1\./));
|
||||
assert.ok(!has(/body-parser 2\.4\.0 ->/) && !has(/body-parser 1\.20\.9 ->/));
|
||||
assert.ok(has(/uuid 8\.3\.2 -> 11\.1\.1/) && !has(/uuid 14\./), 'uuid 14 is above the minimum');
|
||||
assert.ok(has(/nodemailer 8\.0\.11 -> 10\.3\.1/));
|
||||
});
|
||||
|
||||
test('a fix that needs a new major its dependents cannot take is recorded in notFixable (negative)', () => {
|
||||
const reason = (manifest.notFixable['postcss-selector-parser'] || []).join(' ');
|
||||
assert.ok(/major/.test(reason) && /CVE-2026-104844/.test(reason));
|
||||
assert.ok(manifest.minimums['postcss-selector-parser'] === '6.1.5',
|
||||
'but the same-major fix that exists is taken');
|
||||
assert.ok(!('lodash.template' in manifest.notFixable));
|
||||
for (const [name, version] of Object.entries(manifest.minimums)) {
|
||||
assert.ok(/^\d+\.\d+\.\d+$/.test(version), `${name} is pinned to an exact version`);
|
||||
|
||||
Reference in new issue
Block a user