Files
DumbDrop/test/path-validation.test.js
abite c574310d86 fix: resolve file disappearance with Docker bind mounts
(#73)

* fix: resolve file disappearance with Docker bind mounts

Files were disappearing when using bind mounts because fs.realpathSync()
requires files to exist. Updated path validation to use path.resolve()
for non-existing files (during upload) and fs.realpathSync() only for
existing files (during operations).

- Add isPathWithinUploadDir() to fileUtils with requireExists parameter
- Update files.js to use shared validation function
- Add path validation to upload.js for all file operations
- Add comprehensive test suite (16 tests, all passing)
- Maintain security against path traversal attacks
- Full backward compatibility with named volumes

Fixes: Files disappearing with bind mounts
Related: d69a8b2, fc8bff9"

* Improve file existence check in isPathWithinUploadDir

Refines logic to immediately return false if requireExists is true and the file does not exist, ensuring more robust handling of non-existent files.

* Bump version to 1.0.1

Update package version from 1.0.0 to 1.0.1 in preparation for a new release.
2025-11-09 20:36:24 -08:00

153 lines
5.7 KiB
JavaScript

/**
* Path validation tests for bind mount compatibility
* Tests the isPathWithinUploadDir function with various scenarios
* Validates both existing and non-existing file paths
*/
const { test, describe, before, after } = require('node:test');
const assert = require('node:assert');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { isPathWithinUploadDir } = require('../src/utils/fileUtils');
describe('Path Validation for Bind Mounts', () => {
let testUploadDir;
before(() => {
// Create a temporary upload directory for testing
testUploadDir = path.join(os.tmpdir(), 'dumbdrop-test-uploads-' + Date.now());
fs.mkdirSync(testUploadDir, { recursive: true });
});
after(() => {
// Clean up test directory
try {
fs.rmSync(testUploadDir, { recursive: true, force: true });
} catch (err) {
console.error('Failed to clean up test directory:', err);
}
});
test('should allow valid file path within upload directory (non-existent file)', () => {
const filePath = path.join(testUploadDir, 'test-file.txt');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should allow valid nested file path within upload directory (non-existent)', () => {
const filePath = path.join(testUploadDir, 'subfolder', 'test-file.txt');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should allow valid file path with spaces (non-existent)', () => {
const filePath = path.join(testUploadDir, 'test file with spaces.txt');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should reject path traversal with ../ (non-existent)', () => {
const filePath = path.join(testUploadDir, '..', 'malicious.txt');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), false);
});
test('should reject path traversal with nested ../ (non-existent)', () => {
const filePath = path.join(testUploadDir, 'folder', '..', '..', 'malicious.txt');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), false);
});
test('should allow upload directory itself', () => {
assert.strictEqual(isPathWithinUploadDir(testUploadDir, testUploadDir, false), true);
});
test('should work with .partial file extensions', () => {
const filePath = path.join(testUploadDir, 'upload.txt.partial');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should handle paths with normalized separators', () => {
// Test with forward slashes (cross-platform)
const filePath = path.normalize(path.join(testUploadDir, 'folder/subfolder/file.txt'));
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should work with existing files when requireExists=true', () => {
// Create an actual file
const filePath = path.join(testUploadDir, 'existing-file.txt');
fs.writeFileSync(filePath, 'test content');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, true), true);
// Clean up
fs.unlinkSync(filePath);
});
test('should reject existing file outside upload directory', () => {
// Create a file outside the upload directory
const outsideFile = path.join(os.tmpdir(), 'outside-file.txt');
fs.writeFileSync(outsideFile, 'test content');
assert.strictEqual(isPathWithinUploadDir(outsideFile, testUploadDir, true), false);
// Clean up
fs.unlinkSync(outsideFile);
});
test('should reject paths on different drives (Windows only)', () => {
if (process.platform !== 'win32') {
return; // Skip on non-Windows
}
// Try to use a different drive letter
const currentDrive = testUploadDir.split(':')[0];
const differentDrive = currentDrive === 'C' ? 'D' : 'C';
const differentDrivePath = `${differentDrive}:\\temp\\file.txt`;
// This should be rejected
assert.strictEqual(isPathWithinUploadDir(differentDrivePath, testUploadDir, false), false);
});
test('should handle deeply nested folder structures', () => {
const deepPath = path.join(testUploadDir, 'a', 'b', 'c', 'd', 'e', 'file.txt');
assert.strictEqual(isPathWithinUploadDir(deepPath, testUploadDir, false), true);
});
test('should reject absolute paths outside upload directory', () => {
const outsidePath = path.join(os.tmpdir(), 'outside', 'file.txt');
assert.strictEqual(isPathWithinUploadDir(outsidePath, testUploadDir, false), false);
});
});
describe('Path Validation Edge Cases', () => {
let testUploadDir;
before(() => {
testUploadDir = path.join(os.tmpdir(), 'dumbdrop-edge-test-' + Date.now());
fs.mkdirSync(testUploadDir, { recursive: true });
});
after(() => {
try {
fs.rmSync(testUploadDir, { recursive: true, force: true });
} catch (err) {
console.error('Failed to clean up test directory:', err);
}
});
test('should handle special characters in filenames', () => {
const filePath = path.join(testUploadDir, 'file (1).txt');
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should handle Unicode filenames', () => {
const filePath = path.join(testUploadDir, 'файл.txt'); // Russian
assert.strictEqual(isPathWithinUploadDir(filePath, testUploadDir, false), true);
});
test('should reject non-existent upload directory', () => {
const fakeUploadDir = path.join(os.tmpdir(), 'non-existent-dir-' + Date.now());
const filePath = path.join(fakeUploadDir, 'file.txt');
assert.strictEqual(isPathWithinUploadDir(filePath, fakeUploadDir, false), false);
});
});