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
1 change: 1 addition & 0 deletions docs/transports.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ looking for daily log rotation see [DailyRotateFile](#dailyrotatefile-transport)
* __silent:__ Boolean flag indicating whether to suppress output (default false).
* __eol:__ Line-ending character to use. (default: `os.EOL`).
* __lazy:__ If true, log files will be created on demand, not at the initialization time.
* __allowSymlinks:__ Boolean flag indicating whether to allow symlinks (default true).
* __filename:__ The filename of the logfile to write output to.
* __maxsize:__ Max size in bytes of the logfile, if the size is exceeded then a new file is created, a counter will become a suffix of the log file.
* __maxFiles:__ Limit the number of files created when the size of the logfile is exceeded.
Expand Down
17 changes: 16 additions & 1 deletion lib/winston/transports/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module.exports = class File extends TransportStream {
this.eol = (typeof options.eol === 'string') ? options.eol : os.EOL;
this.tailable = options.tailable || false;
this.lazy = options.lazy || false;
this.allowSymlinks = (typeof options.allowSymlinks === 'boolean') ? options.allowSymlinks : true;

// Internal state variables representing the number of files this instance
// has created and the current size (in bytes) of the current logfile.
Expand Down Expand Up @@ -610,7 +611,21 @@ module.exports = class File extends TransportStream {
const fullpath = path.join(this.dirname, this.filename);

debug('create stream start', fullpath, this.options);
const dest = fs.createWriteStream(fullpath, this.options)

const streamOptions = Object.assign({}, this.options);
if (!this.allowSymlinks) {
/* eslint-disable no-bitwise */
if (streamOptions.flags === 'a') {
streamOptions.flags = fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW;
} else if (streamOptions.flags === 'w') {
streamOptions.flags = fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW;
} else if (typeof streamOptions.flags === 'number') {
streamOptions.flags |= fs.constants.O_NOFOLLOW;
}
/* eslint-enable no-bitwise */
}

const dest = fs.createWriteStream(fullpath, streamOptions)
// TODO: What should we do with errors here?
.on('error', err => debug(err))
.on('close', () => debug('close', dest.path, dest.bytesWritten))
Expand Down
1 change: 1 addition & 0 deletions lib/winston/transports/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ declare namespace winston {
eol?: string;
tailable?: boolean;
lazy?: boolean;
allowSymlinks?: boolean;
}

interface FileTransportInstance extends Transport {
Expand Down
147 changes: 147 additions & 0 deletions test/unit/winston/transports/file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,153 @@ describe('File Transport', function () {
});
});

describe('Symlink Option', function () {
it('should not write to a symlink when allowSymlinks is false', async function () {
const targetFilename = 'target.log';
const symlinkFilename = 'symlink.log';
const targetPath = getFilePath(targetFilename);
const symlinkPath = getFilePath(symlinkFilename);

// Create target file
fs.writeFileSync(targetPath, 'initial content');

// Create symlink
try {
fs.symlinkSync(targetPath, symlinkPath);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}

const transport = new winston.transports.File({
dirname: testLogFixturesPath,
filename: symlinkFilename,
allowSymlinks: false
});

// We expect this to fail silently or emit an error, but definitely NOT write to target
// Using a small amount of data to avoid filling the buffer and hanging
await logToTransport(transport, { kbytes: 1 });

// Give it a moment to potentially flush (or fail)
await new Promise(resolve => setTimeout(resolve, 100));

const content = fs.readFileSync(targetPath, 'utf8');
assert.strictEqual(
content,
'initial content',
'Target file should not be modified'
);
});

it('should not write to a symlink when allowSymlinks is false and flags is "w"', async function () {
const targetFilename = 'target_w.log';
const symlinkFilename = 'symlink_w.log';
const targetPath = getFilePath(targetFilename);
const symlinkPath = getFilePath(symlinkFilename);

// Create target file
fs.writeFileSync(targetPath, 'initial content');

// Create symlink
try {
fs.symlinkSync(targetPath, symlinkPath);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}

const transport = new winston.transports.File({
dirname: testLogFixturesPath,
filename: symlinkFilename,
allowSymlinks: false,
options: { flags: 'w' }
});

await logToTransport(transport, { kbytes: 1 });

// Give it a moment to potentially flush (or fail)
await new Promise(resolve => setTimeout(resolve, 100));

const content = fs.readFileSync(targetPath, 'utf8');
assert.strictEqual(
content,
'initial content',
'Target file should not be modified'
);
});

it('should not write to a symlink when allowSymlinks is false and flags is number', async function () {
const targetFilename = 'target_num.log';
const symlinkFilename = 'symlink_num.log';
const targetPath = getFilePath(targetFilename);
const symlinkPath = getFilePath(symlinkFilename);

// Create target file
fs.writeFileSync(targetPath, 'initial content');

// Create symlink
try {
fs.symlinkSync(targetPath, symlinkPath);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}

const transport = new winston.transports.File({
dirname: testLogFixturesPath,
filename: symlinkFilename,
allowSymlinks: false,
/* eslint-disable no-bitwise */
options: { flags: fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY }
/* eslint-enable no-bitwise */
});

await logToTransport(transport, { kbytes: 1 });

// Give it a moment to potentially flush (or fail)
await new Promise(resolve => setTimeout(resolve, 100));

const content = fs.readFileSync(targetPath, 'utf8');
assert.strictEqual(
content,
'initial content',
'Target file should not be modified'
);
});

it('should write to a symlink when allowSymlinks is true (default)', async function () {
const targetFilename = 'target_default.log';
const symlinkFilename = 'symlink_default.log';
const targetPath = getFilePath(targetFilename);
const symlinkPath = getFilePath(symlinkFilename);

// Create target file
fs.writeFileSync(targetPath, 'initial content');

// Create symlink
try {
fs.symlinkSync(targetPath, symlinkPath);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}

const transport = new winston.transports.File({
dirname: testLogFixturesPath,
filename: symlinkFilename
// allowSymlinks: true is default
});

await logToTransport(transport, { kbytes: 1 });

// Wait for write
await new Promise(resolve => setTimeout(resolve, 500));

const content = fs.readFileSync(targetPath, 'utf8');
assert.notStrictEqual(
content,
'initial content',
'Target file should be modified'
);
});
});

// TODO: Reintroduce these tests
//
Expand Down