mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* fix(security): canonicalize file-path-guard and restrict FILE_SYNC_* to outside userData file-path-guard now canonicalizes via fs.realpathSync.native (deepest existing ancestor) before the path.relative check, so symlinks and case-insensitive / 8.3 filesystem aliases can't bypass containment — this also hardens the existing backup-dir (GHSA-x937) check, which was purely lexical and bypassable on macOS. Adds assertPathOutside and applies it to every FILE_SYNC_* handler and READ_LOCAL_IMAGE_AS_DATA_URL in local-file-sync.ts, so the renderer (untrusted plugin/XSS) can no longer read/write/delete/list/inline files inside the app's private dir (userData: settings/grants/db) while user-chosen sync folders still work. * fix(security): validate clipboard image basePath, fileName and copy source ipc-handler-wrapper now uses isPathInsideDir containment (not a bare startsWith, which accepted a sibling like <userData>-evil) and rejects any fileName that is not a plain image basename or imageId containing separators/.. — closing the clipboard grant-file forgery (fileName='simpleSettings'). CLIPBOARD_COPY_IMAGE_FILE now requires an image-extension source. The supported-image-extension allowlist is consolidated into a single SUPPORTED_IMAGE_EXTENSIONS in mime-type-mapping.
84 lines
2.6 KiB
JavaScript
84 lines
2.6 KiB
JavaScript
/**
|
|
* Security test for CLIPBOARD_COPY_IMAGE_FILE source validation.
|
|
* The copy source is renderer-supplied; only actual image files may be copied
|
|
* (so a plugin/XSS can't copy a secret into the images dir and read it back).
|
|
* Run with: npm run test:electron
|
|
*/
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const fs = require('node:fs');
|
|
const Module = require('node:module');
|
|
|
|
require('ts-node/register/transpile-only');
|
|
|
|
const modPath = path.resolve(__dirname, 'clipboard-image-handler.ts');
|
|
const originalModuleLoad = Module._load;
|
|
|
|
let handlers = {};
|
|
let userDataDir;
|
|
let externalDir;
|
|
|
|
const installMocks = () => {
|
|
Module._load = function (request, parent, isMain) {
|
|
if (request === 'electron') {
|
|
return {
|
|
ipcMain: { handle: (channel, fn) => (handlers[channel] = fn) },
|
|
app: { getPath: () => userDataDir },
|
|
clipboard: { readImage: () => ({ isEmpty: () => true }) },
|
|
};
|
|
}
|
|
return originalModuleLoad.call(this, request, parent, isMain);
|
|
};
|
|
};
|
|
|
|
const load = () => {
|
|
delete require.cache[modPath];
|
|
handlers = {};
|
|
require(modPath).initClipboardImageHandlers();
|
|
};
|
|
|
|
test.beforeEach(() => {
|
|
userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-cb-ud-'));
|
|
externalDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-cb-src-'));
|
|
installMocks();
|
|
load();
|
|
});
|
|
|
|
test.afterEach(() => {
|
|
Module._load = originalModuleLoad;
|
|
delete require.cache[modPath];
|
|
fs.rmSync(userDataDir, { recursive: true, force: true });
|
|
fs.rmSync(externalDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const imagesDir = () => path.join(userDataDir, 'clipboard-images');
|
|
|
|
test('CLIPBOARD_COPY_IMAGE_FILE refuses a non-image source file', async () => {
|
|
const secret = path.join(externalDir, 'secret.txt');
|
|
fs.writeFileSync(secret, 'topsecret');
|
|
|
|
const result = await handlers['CLIPBOARD_COPY_IMAGE_FILE'](
|
|
{},
|
|
{ basePath: imagesDir(), filePath: secret },
|
|
);
|
|
|
|
assert.equal(result, null, 'returns the error value');
|
|
// Nothing copied into the images dir.
|
|
const copied = fs.existsSync(imagesDir()) ? fs.readdirSync(imagesDir()) : [];
|
|
assert.equal(copied.length, 0);
|
|
});
|
|
|
|
test('CLIPBOARD_COPY_IMAGE_FILE copies an actual image file', async () => {
|
|
const src = path.join(externalDir, 'pic.png');
|
|
fs.writeFileSync(src, 'pngbytes');
|
|
|
|
const result = await handlers['CLIPBOARD_COPY_IMAGE_FILE'](
|
|
{},
|
|
{ basePath: imagesDir(), filePath: src },
|
|
);
|
|
|
|
assert.ok(result && typeof result.id === 'string');
|
|
assert.equal(fs.readdirSync(imagesDir()).length, 1);
|
|
});
|