mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
- imports/i18n/i18n.test.js crashed the whole run at load: chai 6.x plugins (sinon-chai,
chai-as-promised) are ESM-only, so use(require('sinon-chai')) handed chai.use() a module
namespace instead of the plugin function ("fn is not a function"). Now imports the default
export.
- That unmasked a second load crash - PositionHistory.helpers is not a function: the
meteor test entry (server/lib/tests/index.js) never ran the .helpers / .attachSchema shim
that server/main.js bootstraps, so the first model to call Collection.helpers({...}) threw.
The shim is now imported first in the test entry.
- With the suite finally running, 44 server tests failed because meteor test only loads what
the specs import (not the app's /server/imports). The specs now import the files that
register the methods / globals under test (cards.vote / cards.pokerVote, api.attachment.*,
cloneBoard, getBackgroundImageURL, applyListWidth, updateListSort, moveChecklist,
userPositionHistory.*, archiveBoard, sendSMTPTestEmail, and the Attachments global). Also
fixed genuine test bugs: the cards.vote / cards.pokerVote specs were written synchronously
against async methods; the header-login trust specs restored env vars with
process.env.X = undefined (which stores the string "undefined" and shadowed the trusted-IP
allowlist, making every trusted source read as untrusted); the DnsBleed decimal-loopback
matcher assumed the integer host survived URL normalisation; and the dependencies-OpenAPI
spec could not locate its source file from the built bundle. The two cards.archive /
cards.move specs tested Meteor methods that do not exist (archive / move are Minimongo
document helpers secured by Cards.allow / Cards.deny, already covered by the cards security
tests) and were removed. Server-side Mocha is now 409 passing, 0 failing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
3.4 KiB
JavaScript
115 lines
3.4 KiB
JavaScript
/* eslint-env mocha */
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import { expect } from 'chai';
|
|
import { Random } from 'meteor/random';
|
|
import { FileStoreStrategyFilesystem } from '/models/lib/fileStoreStrategy';
|
|
import Attachments from '/models/attachments';
|
|
|
|
// FileStoreStrategyFilesystem falls back to a bare global `Attachments` when no
|
|
// collection is passed (models/lib/fileStoreStrategy.js). The app exposes model
|
|
// collections as globals; the meteor-test module scope does not, so define it
|
|
// here to avoid a ReferenceError when constructing the strategy.
|
|
globalThis.Attachments = globalThis.Attachments || Attachments;
|
|
|
|
function readStreamToString(stream) {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks = [];
|
|
stream.on('data', (chunk) => chunks.push(chunk));
|
|
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
stream.on('error', reject);
|
|
});
|
|
}
|
|
|
|
describe('fileStoreStrategy security', function() {
|
|
let tempRoot;
|
|
let storageRoot;
|
|
let originalWritablePath;
|
|
|
|
beforeEach(function() {
|
|
originalWritablePath = process.env.WRITABLE_PATH;
|
|
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wekan-fss-'));
|
|
storageRoot = path.join(tempRoot, 'files', 'attachments');
|
|
fs.mkdirSync(storageRoot, { recursive: true });
|
|
process.env.WRITABLE_PATH = tempRoot;
|
|
});
|
|
|
|
afterEach(function() {
|
|
if (typeof originalWritablePath === 'string') {
|
|
process.env.WRITABLE_PATH = originalWritablePath;
|
|
} else {
|
|
delete process.env.WRITABLE_PATH;
|
|
}
|
|
|
|
try {
|
|
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
} catch (err) {
|
|
// Ignore cleanup errors in tests.
|
|
}
|
|
});
|
|
|
|
function createAttachmentFileObj(filePath) {
|
|
return {
|
|
_id: Random.id(),
|
|
name: 'proof.txt',
|
|
collectionName: 'attachments',
|
|
versions: {
|
|
original: {
|
|
path: filePath,
|
|
storage: 'fs',
|
|
type: 'text/plain',
|
|
size: 4,
|
|
meta: {},
|
|
},
|
|
},
|
|
meta: {
|
|
boardId: Random.id(),
|
|
},
|
|
};
|
|
}
|
|
|
|
it('denies absolute path outside attachment storage root', function() {
|
|
const externalPath = path.join(tempRoot, 'outside-secret.txt');
|
|
fs.writeFileSync(externalPath, 'secret');
|
|
|
|
const strategy = new FileStoreStrategyFilesystem(
|
|
createAttachmentFileObj(externalPath),
|
|
'original',
|
|
);
|
|
|
|
const stream = strategy.getReadStream();
|
|
expect(stream).to.equal(undefined);
|
|
});
|
|
|
|
it('allows reading a regular file from storage root', async function() {
|
|
const insidePath = path.join(storageRoot, 'safe.txt');
|
|
fs.writeFileSync(insidePath, 'safe-data');
|
|
|
|
const strategy = new FileStoreStrategyFilesystem(
|
|
createAttachmentFileObj(insidePath),
|
|
'original',
|
|
);
|
|
|
|
const stream = strategy.getReadStream();
|
|
expect(stream).to.not.equal(undefined);
|
|
|
|
const content = await readStreamToString(stream);
|
|
expect(content).to.equal('safe-data');
|
|
});
|
|
|
|
it('denies symlink inside storage root that points outside', function() {
|
|
const externalPath = path.join(tempRoot, 'outside-via-link.txt');
|
|
fs.writeFileSync(externalPath, 'linked-secret');
|
|
const symlinkPath = path.join(storageRoot, 'escape-link.txt');
|
|
fs.symlinkSync(externalPath, symlinkPath);
|
|
|
|
const strategy = new FileStoreStrategyFilesystem(
|
|
createAttachmentFileObj(symlinkPath),
|
|
'original',
|
|
);
|
|
|
|
const stream = strategy.getReadStream();
|
|
expect(stream).to.equal(undefined);
|
|
});
|
|
}); |