mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-16 23:33:10 -06:00
Clear the high-severity advisories that reach production
npm audit reports 8 high findings. Four of them reach production; the other four are
dev-only and cannot, because prod installs with --omit=dev. Verified rather than
assumed: puppeteer-core, extract-zip, @puppeteer/browsers and js-yaml are all absent
from prod's node_modules.
Three of the four are transitive, and the fix is a patch or minor inside the range
package.json already declares — no API moves, and package.json is untouched by them:
brace-expansion 2.1.2 -> 2.1.4 (archiver -> glob/minimatch)
ip-address 10.2.0 -> 10.5.0 (express-rate-limit)
socket.io-parser 4.2.6 -> 4.2.7 (socket.io)
socket.io-parser was the one worth checking, because a parser change that altered the
wire format would break every deployed player at once rather than fail a test. It does
not: socket.io stays at 4.8.3, engine.io at 6.6.9, and the parser's protocol constant
is still 5. Nothing a player speaks changes.
The fourth is a real bump — nodemailer 6.10.1 -> 9.0.5, across three majors, closing
eight advisories including SMTP command injection and header injection. Our surface is
about as small as it gets: createTransport({host, port, secure, auth}) and sendMail with
from/to/subject/text/html. Engine requirements are unchanged (>=6.0.0), and the entry
point is the same.
The existing email tests mock nodemailer through require.cache, so they would have
stayed green through any breaking change in the library itself — proven, not guessed:
with sendMail patched to throw, those 15 tests still pass. So this adds a test that
drives the REAL library over a loopback SMTP server and asserts on the conversation,
using messages built by our own buildSmtpMessage rather than hand-written ones. That
test does fail against the broken build.
Left alone: extract-zip under puppeteer-core, now with no fix available. It is a
devDependency used only by smoke-ui.js, which already no-ops when it is missing, and the
advisory is symlink traversal while unpacking a downloaded browser — puppeteer-core with
an explicit executablePath never downloads or extracts one.
Production audit goes from 4 high to 0. 1671/1671 pass.
This commit is contained in:
parent
702e107972
commit
dce63e4050
32
server/package-lock.json
generated
32
server/package-lock.json
generated
|
|
@ -21,7 +21,7 @@
|
|||
"jimp": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"nodemailer": "^9.0.5",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"socket.io": "^4.7.2",
|
||||
|
|
@ -1941,9 +1941,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
|
||||
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
|
|
@ -3434,9 +3434,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
|
||||
"integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
|
|
@ -3544,9 +3544,9 @@
|
|||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -3913,9 +3913,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.5.tgz",
|
||||
"integrity": "sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
|
@ -4931,9 +4931,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||
"version": "4.2.7",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
|
||||
"integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"jimp": "^1.6.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.16",
|
||||
"nodemailer": "^9.0.5",
|
||||
"otplib": "^12.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"socket.io": "^4.7.2",
|
||||
|
|
|
|||
94
server/test/email-nodemailer-live.test.js
Normal file
94
server/test/email-nodemailer-live.test.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// The rest of the email tests mock nodemailer through require.cache, which proves our code
|
||||
// calls sendMail correctly but says nothing about whether nodemailer still ACCEPTS what we
|
||||
// hand it. That gap is exactly where a dependency bump breaks sending: the suite stays green
|
||||
// while mail stops leaving the building.
|
||||
//
|
||||
// This drives the real library against a throwaway SMTP server on a loopback port, using the
|
||||
// option and message shapes services/email.js actually builds. No network, no credentials.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const net = require('node:net');
|
||||
const nodemailer = require('nodemailer');
|
||||
const { buildSmtpMessage } = require('../services/email');
|
||||
|
||||
// A minimal SMTP server that answers enough of the protocol to accept one message and
|
||||
// records the conversation, so assertions can be made about what went over the wire.
|
||||
function startFakeSmtp() {
|
||||
const seen = { commands: [], data: '' };
|
||||
return new Promise((resolve) => {
|
||||
const srv = net.createServer((sock) => {
|
||||
let inData = false;
|
||||
sock.write('220 fake.local ESMTP\r\n');
|
||||
sock.on('data', (buf) => {
|
||||
const chunk = buf.toString();
|
||||
if (inData) {
|
||||
seen.data += chunk;
|
||||
if (/\r\n\.\r\n/.test(seen.data)) { inData = false; sock.write('250 OK queued\r\n'); }
|
||||
return;
|
||||
}
|
||||
for (const line of chunk.split('\r\n').filter(Boolean)) {
|
||||
seen.commands.push(line);
|
||||
const cmd = line.split(' ')[0].toUpperCase();
|
||||
if (cmd === 'EHLO' || cmd === 'HELO') sock.write('250-fake.local\r\n250 SIZE 10485760\r\n');
|
||||
else if (cmd === 'MAIL' || cmd === 'RCPT') sock.write('250 OK\r\n');
|
||||
else if (cmd === 'DATA') { inData = true; sock.write('354 send it\r\n'); }
|
||||
else if (cmd === 'QUIT') { sock.write('221 bye\r\n'); sock.end(); }
|
||||
else sock.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
sock.on('error', () => {}); // a client hanging up mid-conversation is not a test failure
|
||||
});
|
||||
srv.listen(0, '127.0.0.1', () => resolve({ srv, port: srv.address().port, seen }));
|
||||
});
|
||||
}
|
||||
|
||||
test('the installed nodemailer accepts the options and messages we build', async () => {
|
||||
const { srv, port, seen } = await startFakeSmtp();
|
||||
try {
|
||||
// The shape getSmtpTransporter() constructs.
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: '127.0.0.1', port, secure: false, tls: { rejectUnauthorized: false },
|
||||
});
|
||||
|
||||
// The object `from` form, used when a display name overrides the configured one.
|
||||
await transporter.sendMail({
|
||||
from: { name: 'ScreenTinker', address: 'noreply@example.com' },
|
||||
to: 'user@x.com',
|
||||
subject: '[ScreenTinker] Hello',
|
||||
html: '<p>hi there</p>',
|
||||
text: 'hi there',
|
||||
});
|
||||
|
||||
assert.ok(seen.commands.some(c => /^EHLO/i.test(c)), 'greets the server');
|
||||
assert.ok(seen.commands.some(c => /^MAIL FROM:<noreply@example\.com>/i.test(c)),
|
||||
'envelope sender is the configured address, not the display name');
|
||||
assert.ok(seen.commands.some(c => /^RCPT TO:<user@x\.com>/i.test(c)), 'envelope recipient');
|
||||
assert.match(seen.data, /Subject: \[ScreenTinker\] Hello/, 'subject and its prefix survive encoding');
|
||||
assert.match(seen.data, /From: ScreenTinker <noreply@example\.com>/, 'display-name form still renders');
|
||||
assert.match(seen.data, /Content-Type: multipart\/alternative/, 'text and html sent as alternatives');
|
||||
|
||||
// The string `from` form, used when there is no override.
|
||||
await transporter.sendMail({
|
||||
from: 'ScreenTinker <noreply@example.com>', to: 'user@x.com', subject: 'Welcome', html: '<p>x</p>',
|
||||
});
|
||||
transporter.close();
|
||||
} finally {
|
||||
srv.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('buildSmtpMessage output is something the installed nodemailer can send', async () => {
|
||||
const { srv, port, seen } = await startFakeSmtp();
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: '127.0.0.1', port, secure: false, tls: { rejectUnauthorized: false },
|
||||
});
|
||||
// Built by our own code rather than hand-written here, so the two cannot drift apart.
|
||||
await transporter.sendMail(buildSmtpMessage('to@x.com', 'Subj', 'plain', '<p>rich</p>', 'Sender Name'));
|
||||
assert.ok(seen.commands.some(c => /^RCPT TO:<to@x\.com>/i.test(c)), 'recipient reached the wire');
|
||||
assert.match(seen.data, /Subject: Subj/, 'subject reached the wire');
|
||||
transporter.close();
|
||||
} finally {
|
||||
srv.close();
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue