Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-geckos-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates)
10 changes: 6 additions & 4 deletions apps/meteor/app/file/server/file.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { NpmModuleMongodb } from 'meteor/npm-mongo';
import mime from 'mime-type/with-db';
import mkdirp from 'mkdirp';

import { sanitizeFileName } from './functions/sanitizeFileName';

const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;

type IFile = {
Expand Down Expand Up @@ -141,23 +143,23 @@ class FileSystem implements IRocketChatFileStore {
}

createWriteStream(fileName: string) {
const ws = fs.createWriteStream(path.join(this.absolutePath, fileName));
const ws = fs.createWriteStream(path.join(this.absolutePath, sanitizeFileName(fileName)));
ws.on('close', () => {
return ws.emit('end');
});
return ws;
}

createReadStream(fileName: string) {
return fs.createReadStream(path.join(this.absolutePath, fileName));
return fs.createReadStream(path.join(this.absolutePath, sanitizeFileName(fileName)));
}

stat(fileName: string) {
return fsp.stat(path.join(this.absolutePath, fileName));
return fsp.stat(path.join(this.absolutePath, sanitizeFileName(fileName)));
}

async remove(fileName: string) {
return fsp.unlink(path.join(this.absolutePath, fileName));
return fsp.unlink(path.join(this.absolutePath, sanitizeFileName(fileName)));
}

async getFileWithReadStream(fileName: string) {
Expand Down
55 changes: 55 additions & 0 deletions apps/meteor/app/file/server/functions/sanitizeFileName.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';

import { sanitizeFileName } from './sanitizeFileName';

describe('sanitizeFileName', () => {
describe('valid filenames', () => {
it('should allow simple filenames', () => {
const result = sanitizeFileName('sound.mp3');
expect(result).to.equal('sound.mp3');
});

it('should allow filenames with dashes, underscores and dots', () => {
const result = sanitizeFileName('alert_test-01.wav');
expect(result).to.equal('alert_test-01.wav');
});

it('should allow filenames without extension', () => {
const result = sanitizeFileName('beep');
expect(result).to.equal('beep');
});
});

describe('invalid paths', () => {
it('should reject path traversal using ../', () => {
expect(() => sanitizeFileName('../etc/passwd')).to.throw();
});

it('should reject path traversal using ./', () => {
expect(() => sanitizeFileName('./folder/sounds')).to.throw();
});

it('should reject nested paths', () => {
expect(() => sanitizeFileName('sounds/alert.mp3')).to.throw();
});

it('should reject absolute paths', () => {
expect(() => sanitizeFileName('/etc/passwd')).to.throw();
});
});

describe('invalid characters', () => {
it('should reject filenames with spaces', () => {
expect(() => sanitizeFileName('my sound.mp3')).to.throw();
});

it('should reject filenames with special characters', () => {
expect(() => sanitizeFileName('sound$.mp3')).to.throw();
});

it('should reject filenames with backslashes', () => {
expect(() => sanitizeFileName('..\\passwd')).to.throw();
});
});
});
19 changes: 19 additions & 0 deletions apps/meteor/app/file/server/functions/sanitizeFileName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import path from 'path';

export function sanitizeFileName(fileName: string) {
const base = path.basename(fileName);

if (base !== fileName) {
throw new Error('error-invalid-file-name');
}

if (base === '.' || base.startsWith('..')) {
throw new Error('error-invalid-file-name');
}

if (!/^[a-zA-Z0-9._-]+$/.test(base)) {
throw new Error('error-invalid-characters-in-file-name');
}

return base;
}
Loading