sandbox-fs
v1.1.0
Published
A read-only virtual file system that maps Unix-style virtual paths to a real directory root
Downloads
194
Maintainers
Readme
sandbox-fs
A read-only virtual file system for Node.js that maps Unix-style virtual paths to a real directory root. Provides a secure sandbox environment where write operations are blocked and real filesystem paths are hidden from error messages.
Features
- 🔒 Read-only: All write operations return
EACCES(access denied) - 🌍 Cross-platform: Works with both Windows and Unix paths as root
- 🔀 Unix-style virtual paths: All virtual paths use forward slashes (
/) - 🚫 Path traversal protection: Prevents escaping the VFS root with
.. - 🎭 Error filtering: Real paths in error messages are replaced with virtual paths
- 📦 Full fs API: Compatible with Node.js
fsmodule (both callback and promise APIs) - 🛤️ Path module: Virtual
pathmodule with fixed CWD of/ - 🧹 Resource cleanup: Proper cleanup of file descriptors and streams
Installation
npm install sandbox-fsRequirements
- Node.js >= 18.0.0
Quick Start
import { VirtualFileSystem } from 'sandbox-fs';
// Create a VFS instance with a real directory as root
const vfs = new VirtualFileSystem({ root: '/opt/data' });
// Get fs and path modules
const fs = vfs.createNodeFSModule();
const path = vfs.createNodePathModule();
// Read files using virtual paths
// Virtual path: /config.json
// Real path: /opt/data/config.json
const config = fs.readFileSync('/config.json', 'utf8');
// Write operations are denied
try {
fs.writeFileSync('/output.txt', 'data');
} catch (err) {
console.log(err.code); // 'EACCES'
}
// Path operations use virtual CWD of '/'
console.log(path.resolve('.')); // '/'
console.log(path.resolve('foo/bar.txt')); // '/foo/bar.txt'
// Clean up when done
vfs.close();API Reference
VirtualFileSystem
The main class for creating a virtual file system instance.
Constructor
new VirtualFileSystem(options: VFSOptions)Creates a new VFS instance.
Parameters:
options.root(string): The real filesystem path to use as the VFS root. Can be a Windows path (C:\data) or Unix path (/opt/data).
Throws:
- Error if the root path doesn't exist or is not a directory
Example:
// Unix
const vfs = new VirtualFileSystem({ root: '/opt/data' });
// Windows
const vfs = new VirtualFileSystem({ root: 'C:\\data' });toRealPath(virtualPath: string): string
Converts a virtual path to a real filesystem path.
Parameters:
virtualPath(string): Virtual path starting with/(Unix-style)
Returns: Real filesystem path (platform-native)
Throws:
- Error if path traversal is detected (e.g.,
/../etc/passwd)
Example:
const vfs = new VirtualFileSystem({ root: 'C:\\data' });
vfs.toRealPath('/foo/bar.txt'); // Returns: 'C:\data\foo\bar.txt'toVirtualPath(realPath: string): string
Converts a real filesystem path to a virtual path.
Parameters:
realPath(string): Real filesystem path (platform-native)
Returns: Virtual path starting with / (Unix-style)
Throws:
- Error if real path is outside the VFS root
Example:
const vfs = new VirtualFileSystem({ root: 'C:\\data' });
vfs.toVirtualPath('C:\\data\\foo\\bar.txt'); // Returns: '/foo/bar.txt'createNodeFSModule(): any
Creates a Node.js fs-compatible module.
Returns: An object compatible with the Node.js fs module
The returned object includes:
- Callback API:
readFile(),stat(),readdir(), etc. - Promise API:
fs.promises.readFile(),fs.promises.stat(), etc. - Sync API:
readFileSync(),statSync(),readdirSync(), etc. - Stream API:
createReadStream()(write streams throw EACCES) - Constants:
fs.constants
Behavior:
- ✅ Read operations: Work normally (readFile, stat, readdir, open, createReadStream, etc.)
- ❌ Write operations: Throw
EACCES(writeFile, mkdir, unlink, etc.) - ❌ Symlink operations: Throw
EACCES(symlink, link, readlink) - 🔒 Error filtering: All error messages replace real paths with virtual paths
Example:
const fs = vfs.createNodeFSModule();
// Callback API
fs.readFile('/file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// Promise API
const data = await fs.promises.readFile('/file.txt', 'utf8');
// Sync API
const data = fs.readFileSync('/file.txt', 'utf8');
// Stream API
const stream = fs.createReadStream('/large-file.txt');
stream.on('data', chunk => console.log(chunk));createNodePathModule(): any
Creates a Node.js path-compatible module.
Returns: An object compatible with the Node.js path module
The returned object includes all path methods operating in virtual path space:
resolve(),join(),normalize(),relative()dirname(),basename(),extname()parse(),format(),isAbsolute()sep,delimiter,posix,win32
Behavior:
- All paths use Unix-style forward slashes (
/) - Virtual CWD is always
/ path.sepis always/path.delimiteris always:- Both
path.posixandpath.win32reference the same virtual implementation
Example:
const path = vfs.createNodePathModule();
path.resolve('.'); // Returns: '/'
path.resolve('foo/bar.txt'); // Returns: '/foo/bar.txt'
path.join('/foo', 'bar'); // Returns: '/foo/bar'
path.dirname('/foo/bar.txt'); // Returns: '/foo'
path.basename('/foo/bar.txt'); // Returns: 'bar.txt'
path.sep; // Returns: '/'close(): void
Closes the VFS and releases all resources.
This method:
- Closes all tracked file descriptors
- Destroys all active streams
- Marks the VFS as closed
- Causes all subsequent fs operations to throw
EBADF
Example:
const vfs = new VirtualFileSystem({ root: '/opt/data' });
const fs = vfs.createNodeFSModule();
const fd = fs.openSync('/file.txt', 'r');
vfs.close(); // Closes fd and all other resources
// All operations now throw EBADF
fs.readFileSync('/file.txt'); // Throws: EBADF (bad file descriptor)closed (getter)
Returns: boolean - Whether the VFS has been closed
if (!vfs.closed) {
vfs.close();
}Security Features
Path Traversal Protection
The VFS validates all paths to prevent escaping the root directory:
const vfs = new VirtualFileSystem({ root: '/opt/data' });
const fs = vfs.createNodeFSModule();
// These throw errors:
fs.readFileSync('/../etc/passwd'); // Error: Path traversal detected
fs.readFileSync('/foo/../../etc/passwd'); // Error: Path traversal detectedError Message Filtering
Real filesystem paths are automatically filtered from error messages:
const vfs = new VirtualFileSystem({ root: '/opt/data' });
const fs = vfs.createNodeFSModule();
try {
fs.readFileSync('/nonexistent.txt');
} catch (err) {
// Real error: ENOENT: no such file or directory, open '/opt/data/nonexistent.txt'
// Filtered error: ENOENT: no such file or directory, open '/nonexistent.txt'
console.log(err.message); // Real path is replaced with virtual path
}Read-Only Enforcement
All write operations return EACCES:
const fs = vfs.createNodeFSModule();
// All of these throw EACCES:
fs.writeFileSync('/file.txt', 'data');
fs.appendFileSync('/file.txt', 'data');
fs.mkdirSync('/newdir');
fs.unlinkSync('/file.txt');
fs.rmdirSync('/dir');
fs.renameSync('/old.txt', '/new.txt');
fs.chmodSync('/file.txt', 0o644);
fs.createWriteStream('/file.txt'); // Throws immediatelySymlink Blocking
All symlink operations return EACCES:
const fs = vfs.createNodeFSModule();
// All of these throw EACCES:
fs.symlinkSync('/target', '/link');
fs.linkSync('/existing', '/new');
fs.readlinkSync('/symlink');
// stat/lstat also throw EACCES if the target is a symlink
const stats = fs.statSync('/symlink'); // Throws EACCESPlatform Support
Windows
- VFS root can be a Windows path:
C:\data,D:\projects, etc. - Virtual paths always use forward slashes:
/foo/bar.txt - UNC paths are supported:
\\server\share
Unix/Linux/macOS
- VFS root is a standard Unix path:
/opt/data,/home/user/sandbox, etc. - Virtual paths use forward slashes:
/foo/bar.txt
Mounts (read-only)
You can mount external directories into the virtual namespace. Mounts are read-only and directory-only. The most-specific virtual path wins when resolving overlaps. Listings ignore symlinks and implicit parents are treated as directories if they have mount children.
const vfs = new VirtualFileSystem({
root: '/opt/data',
mounts: [{ virtual: '/app', real: '/opt/another' }]
});
// / maps to /opt/data; /app maps to /opt/another
// readdirSync('/') will include 'app' even if /opt/data/app is absentAdvanced Usage
AbortSignal Support
Many fs methods support AbortSignal for cancellation:
const controller = new AbortController();
const fs = vfs.createNodeFSModule();
// Cancel operation after 1 second
setTimeout(() => controller.abort(), 1000);
try {
const data = await fs.promises.readFile('/large-file.txt', {
signal: controller.signal
});
} catch (err) {
if (err.name === 'AbortError') {
console.log('Operation cancelled');
}
}File Descriptor Tracking
The VFS tracks all open file descriptors:
const fs = vfs.createNodeFSModule();
const fd = fs.openSync('/file.txt', 'r');
const buffer = Buffer.alloc(100);
// Read using the file descriptor
fs.readSync(fd, buffer, 0, 100, 0);
// VFS tracks the FD and ensures it's valid
fs.closeSync(fd);
// After close, the FD is invalid
fs.readSync(fd, buffer, 0, 100, 0); // Throws EBADFStream Path Override
Streams have their path property overridden to return virtual paths:
const fs = vfs.createNodeFSModule();
const stream = fs.createReadStream('/data.txt');
console.log(stream.path); // '/data.txt' (virtual path)
// Real path is hiddenWorking with Newer Node.js APIs
The VFS supports newer Node.js APIs (when available):
const fs = vfs.createNodeFSModule();
// glob (Node.js 22+)
if (fs.promises.glob) {
for await (const file of fs.promises.glob('**/*.txt')) {
console.log(file); // Virtual paths
}
}
// statfs (Node.js 19+)
if (fs.promises.statfs) {
const stats = await fs.promises.statfs('/');
console.log(stats);
}
// openAsBlob (Node.js 19+)
if (fs.openAsBlob) {
const blob = await fs.openAsBlob('/file.txt');
}TypeScript Support
Full TypeScript support with type definitions:
import { VirtualFileSystem, VFSOptions } from 'sandbox-fs';
const options: VFSOptions = { root: '/opt/data' };
const vfs: VirtualFileSystem = new VirtualFileSystem(options);
const fs = vfs.createNodeFSModule();
const path = vfs.createNodePathModule();
// Full type inference for fs and path modules
const content: string = fs.readFileSync('/file.txt', 'utf8');
const resolved: string = path.resolve('foo', 'bar');Limitations
- Read-only: Write operations are not supported and will throw
EACCES - Symlinks: Symlink operations and reading symlink targets throw
EACCES - Virtual CWD: The path module always uses
/as the current working directory - Performance: Path conversion adds minimal overhead to each operation
- Node.js version: Newer APIs (glob, statfs) require recent Node.js versions
License
MIT
Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
