Critical severity9.8OSV Advisory· Published Aug 19, 2025· Updated Apr 15, 2026
CVE-2025-55294
CVE-2025-55294
Description
screenshot-desktop allows capturing a screenshot of your local machine. This vulnerability is a command injection issue. When user-controlled input is passed into the format option of the screenshot function, it is interpolated into a shell command without sanitization. This results in arbitrary command execution with the privileges of the calling process. This vulnerability is fixed in 1.15.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
screenshot-desktopnpm | < 1.15.2 | 1.15.2 |
Affected products
1- Range: v1.0.0, v1.1.0, v1.10.0, …
Patches
22 files changed · +3 −3
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "screenshot-desktop", - "version": "1.15.1", + "version": "1.15.2", "description": "Capture a screenshot of your local machine", "main": "index.js", "dependencies": {
package-lock.json+2 −2 modified@@ -1,12 +1,12 @@ { "name": "screenshot-desktop", - "version": "1.15.1", + "version": "1.15.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "screenshot-desktop", - "version": "1.15.1", + "version": "1.15.2", "funding": [ { "type": "github",
59c87b0c175efix: input validation and switch to execFile (#315)
5 files changed · +117 −62
lib/darwin/index.js+32 −23 modified@@ -20,13 +20,21 @@ function darwinSnapshot (options = {}) { return reject(new Error(`Invalid choice of displayId: ${displayId} ${validChoiceMsg}`)) } - const format = options.format || 'jpg' - let filename - let suffix + // Validate format + const allowedFormats = ['jpg', 'jpeg', 'png', 'tiff', 'bmp', 'gif', 'pdf'] + const format = (options.format || 'jpg').toLowerCase() + if (!allowedFormats.includes(format)) { + return reject(new Error('Invalid format')) + } + + // Sanitize filename + let filename, suffix if (options.filename) { - const ix = options.filename.lastIndexOf('.') - suffix = ix >= 0 ? options.filename.slice(ix) : `.${format}` - filename = '"' + options.filename.replace(/"/g, '\\"') + '"' + // Only allow safe characters in filename + const safeFilename = options.filename.replace(/[^a-zA-Z0-9._\-/]/g, '') + const ix = safeFilename.lastIndexOf('.') + suffix = ix >= 0 ? safeFilename.slice(ix) : `.${format}` + filename = safeFilename } else { suffix = `.${format}` } @@ -41,23 +49,24 @@ function darwinSnapshot (options = {}) { } pathsToUse = tmpPaths.slice(0, displayId + 1) - exec('screencapture' + ' -x -t ' + format + ' ' + pathsToUse.join(' '), - function (err, stdOut) { - if (err) { - return reject(err) - } else if (options.filename) { - resolve(path.resolve(options.filename)) - } else { - fs.readFile(tmpPaths[displayId], function (err, img) { - if (err) { - return reject(err) - } - Promise.all(pathsToUse.map(unlinkP)) - .then(() => resolve(img)) - .catch((err) => reject(err)) - }) - } - }) + // Use execFile for safe argument passing + const args = ['-x', '-t', format, ...pathsToUse] + require('child_process').execFile('screencapture', args, function (err, stdOut) { + if (err) { + return reject(err) + } else if (options.filename) { + resolve(path.resolve(filename)) + } else { + fs.readFile(tmpPaths[displayId], function (err, img) { + if (err) { + return reject(err) + } + Promise.all(pathsToUse.map(unlinkP)) + .then(() => resolve(img)) + .catch((err) => reject(err)) + }) + } + }) }) return listDisplays().then((displays) => { return performScreenCapture(displays) })
lib/linux/index.js+22 −16 modified@@ -149,37 +149,43 @@ function linuxSnapshot (options = {}) { listDisplays().then((screens) => { const screen = screens.find(options.screen ? screen => screen.id === options.screen : screen => screen.primary || screen.id === 'default') || screens[0] - const filename = options.filename ? (options.filename.replace(/"/g, '\\"')) : '-' + // Validate format + const allowedFormats = ['jpg', 'jpeg', 'png', 'tiff', 'bmp', 'gif'] + const filetype = (options.format || guessFiletype(options.filename || '')).toLowerCase() + if (!allowedFormats.includes(filetype)) { + return reject(new Error('Invalid format')) + } + + // Sanitize filename + const filename = options.filename ? options.filename.replace(/[^a-zA-Z0-9._\-/]/g, '') : '-' const execOptions = options.filename ? {} : { encoding: 'buffer', maxBuffer: maxBuffer(screens) } - const filetype = options.format || guessFiletype(filename) - let commandLine = '' + let cmd, args switch (options.linuxLibrary) { - case 'scrot': // Faster. Does not support crop. - commandLine = `scrot "${filename}" -e -z "echo \\"${filename}\\""` + case 'scrot': + cmd = 'scrot' + args = [filename, '-e', '-z', 'echo "' + filename + '"'] break case 'imagemagick': default: - commandLine = `import -silent -window root -crop ${screen.crop} -screen ${filetype}:"${filename}" ` + cmd = 'import' + args = ['-silent', '-window', 'root', '-crop', screen.crop, '-screen', filetype + ':' + filename] break } - exec( - commandLine, - execOptions, - (err, stdout) => { - if (err) { - return reject(err) - } else { - return resolve(options.filename ? path.resolve(options.filename) : stdout) - } - }) + require('child_process').execFile(cmd, args, execOptions, (err, stdout) => { + if (err) { + return reject(err) + } else { + return resolve(options.filename ? path.resolve(filename) : stdout) + } + }) }) }) }
lib/utils.js+9 −3 modified@@ -27,10 +27,16 @@ function readAndUnlinkP (path) { readFileP(path) .then((img) => { unlinkP(path) - .then(() => resolve(img)) - .catch(reject) + .then(() => { + resolve(img) + }) + .catch((err) => { + reject(err) + }) + }) + .catch((err) => { + reject(err) }) - .catch(reject) }) }
lib/win32/index.js+51 −17 modified@@ -32,31 +32,65 @@ function copyToTemp () { function windowsSnapshot (options = {}) { return new Promise((resolve, reject) => { - const displayName = options.screen - const format = options.format || 'jpg' - const tmpPath = temp.path({ - suffix: `.${format}` - }) - const imgPath = path.resolve(options.filename || tmpPath) + // Validate format + const allowedFormats = ['jpg', 'jpeg', 'png', 'bmp'] + const format = (options.format || 'jpg').toLowerCase() + if (!allowedFormats.includes(format)) { + return reject(new Error('Invalid format')) + } - const displayChoice = displayName ? ` /d "${displayName}"` : '' + // Sanitize filename (allow spaces) + let imgPath + let requestedPath = null + if (options.filename) { + // Always use a temp path for screenshot output to avoid issues + imgPath = temp.path({ suffix: `.${format}` }) + // Prepare requested path for final output + const originalDir = path.dirname(options.filename) + const originalBase = path.basename(options.filename) + const safeBase = originalBase.replace(/[^a-zA-Z0-9._\- ]/g, '') + requestedPath = path.isAbsolute(options.filename) + ? path.join(originalDir, safeBase) + : path.join(process.cwd(), originalDir, safeBase) + requestedPath = path.normalize(requestedPath) + // Ensure output directory exists + const outDir = path.dirname(requestedPath) + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }) + } + } else { + imgPath = temp.path({ suffix: `.${format}` }) + } + // ...existing code... - const tmpBat = copyToTemp() + // Sanitize displayName + const displayName = options.screen ? String(options.screen).replace(/[^a-zA-Z0-9._\-/]/g, '') : '' - exec('"' + tmpBat + '" "' + imgPath + '" ' + displayChoice, { + const tmpBat = copyToTemp() + const batArgs = [imgPath] + if (displayName) { + batArgs.push('/d', displayName) + } + const args = ['/c', tmpBat, ...batArgs] + // ...existing code... + require('child_process').execFile('cmd.exe', args, { cwd: path.join(os.tmpdir(), 'screenCapture'), windowsHide: true - }, (err, stdout) => { + }, (err, stdout, stderr) => { + // ...existing code... if (err) { return reject(err) + } + if (options.filename) { + // Copy temp file to requested path + fs.copyFile(imgPath, requestedPath, (err) => { + if (err) return reject(err) + resolve(requestedPath) + }) } else { - if (options.filename) { - resolve(imgPath) - } else { - readAndUnlinkP(tmpPath) - .then(resolve) - .catch(reject) - } + readAndUnlinkP(imgPath) + .then(resolve) + .catch(reject) } }) })
test.js+3 −3 modified@@ -43,7 +43,7 @@ test('screenshot to a file', t => { const tmpName = tempPathSync({ suffix: '.jpg' }) return screenshot({ filename: tmpName }).then(() => { t.truthy(existsSync(tmpName)) - unlinkSync(tmpName) + if (existsSync(tmpName)) unlinkSync(tmpName) }) }) @@ -52,7 +52,7 @@ test('screenshot specific screen to a file', t => { const tmpName = tempPathSync({ suffix: '.jpg' }) return screenshot({ filename: tmpName, screen: 0 }).then(() => { t.truthy(existsSync(tmpName)) - unlinkSync(tmpName) + if (existsSync(tmpName)) unlinkSync(tmpName) }) }) @@ -62,7 +62,7 @@ test('screenshot to a file with a space', t => { const tmpName = tempPathSync({ suffix: '.jpg' }) return screenshot({ filename: tmpName }).then(() => { t.truthy(existsSync(tmpName)) - unlinkSync(tmpName) + if (existsSync(tmpName)) unlinkSync(tmpName) }) })
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
4News mentions
0No linked articles in our index yet.