├── .gitattributes ├── test └── file.ext ├── .gitignore ├── .npmignore ├── AUTHORS ├── .prettierrc ├── .eslintrc.json ├── .editorconfig ├── .github └── workflows │ └── test.yml ├── test.js ├── package.json ├── README.md ├── CHANGELOG.md ├── LICENSE └── sandboxed-fs.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * -text 2 | -------------------------------------------------------------------------------- /test/file.ext: -------------------------------------------------------------------------------- 1 | File for test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Mykola Bilochub 2 | Timur Shemsedinov 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "overrides": [ 5 | { 6 | "files": ["**/.*rc", "**/*.json"], 7 | "options": { "parser": "json" } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["metarhia", "plugin:prettier/recommended"], 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": "latest" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [{*.js,*.mjs,*.ts,*.json,*.yml}] 11 | indent_size = 2 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Testing CI 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | node: 12 | - 14 13 | - 16 14 | - 18 15 | - 19 16 | - 20 17 | os: 18 | - ubuntu-latest 19 | - windows-latest 20 | - macos-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node }} 28 | - uses: actions/cache@v2 29 | with: 30 | path: ~/.npm 31 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 32 | restore-keys: | 33 | ${{ runner.os }}-node- 34 | - run: npm ci 35 | - run: npm test 36 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sandboxedFs = require('.'); 4 | const fs = sandboxedFs.bind('./test'); 5 | 6 | fs.readFile('file.ext', (err, data) => { 7 | if (err) { 8 | console.log('Cannot read file'); 9 | return; 10 | } 11 | console.log(data.toString()); 12 | }); 13 | 14 | fs.readFile('../../file.ext', (err, data) => { 15 | if (err) { 16 | console.log('Cannot read file'); 17 | return; 18 | } 19 | console.log(data.toString()); 20 | }); 21 | 22 | (async () => { 23 | let data = undefined; 24 | try { 25 | data = await fs.promises.readFile('file.ext'); 26 | } catch (e) { 27 | console.log('Cannot read file'); 28 | } 29 | if (data) console.log(data.toString()); 30 | })(); 31 | 32 | (async () => { 33 | let data = undefined; 34 | try { 35 | data = await fs.promises.readFile('../../file.ext'); 36 | } catch (e) { 37 | console.log('Cannot read file'); 38 | } 39 | if (data) console.log(data.toString()); 40 | })(); 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandboxed-fs", 3 | "version": "0.3.2", 4 | "author": "Mykola Bilochub ", 5 | "description": "Sandboxed Wrapper for Node.js File System API", 6 | "license": "MIT", 7 | "keywords": [ 8 | "fs", 9 | "metarhia", 10 | "impress", 11 | "sandbox", 12 | "sandboxed", 13 | "application" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/metarhia/sandboxed-fs" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/metarhia/sandboxed-fs/issues", 21 | "email": "nbelochub@gmail.com" 22 | }, 23 | "main": "sandboxed-fs.js", 24 | "files": [], 25 | "devDependencies": { 26 | "eslint": "^8.44.0", 27 | "eslint-config-metarhia": "^8.2.0", 28 | "eslint-config-prettier": "^8.6.0", 29 | "eslint-plugin-import": "^2.27.5", 30 | "eslint-plugin-prettier": "^5.0.0-alpha.2", 31 | "prettier": "^3.0.0" 32 | }, 33 | "engines": { 34 | "node": ">=8.0.0" 35 | }, 36 | "readmeFilename": "README.md", 37 | "scripts": { 38 | "test": "npm run lint && node test.js", 39 | "lint": "eslint . && prettier -c \"**/*.js\" \"**/*.json\" \"**/*.md\" \".*rc\" \"**/*.yml\"", 40 | "fmt": "prettier --write \"**/*.js\" \"**/*.json\" \"**/*.md\" \".*rc\" \"**/*.yml\"" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | sandboxed-fs 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |

24 | 25 | `sandboxed-fs` is a sandboxed wrapper for Node.js file system module implementing 26 | the same API but bound to a certain directory, reliably locked in it. 27 | 28 | ## Usage 29 | 30 | - Install: `npm install sandboxed-fs` 31 | - Require: `const fs = require('sandboxed-fs').bind(path);` 32 | 33 | ## Examples: 34 | 35 | ```javascript 36 | const fs = require('sandboxed-fs').bind(path); 37 | 38 | fs.readFile('file.ext', (err, data) => { 39 | if (err) return console.log('Cannot read file'); 40 | }); 41 | 42 | fs.readFile('../../file.ext', (err, data) => { 43 | if (err) return console.log('Cannot read file'); 44 | }); 45 | ``` 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased][unreleased] 10 | 11 | ### Added 12 | 13 | - CHANGELOG.md file. 14 | 15 | ### Removed 16 | 17 | - Dropped support for Node.js 6. 18 | 19 | ## [0.3.2][] - 2018-06-27 20 | 21 | ### Fixed 22 | 23 | - Issues in the `README.md` file. 24 | 25 | ## [0.3.1][] - 2018-05-10 26 | 27 | ### Added 28 | 29 | - Argument to control enabling access to `os.tmpdir()`. 30 | 31 | ### Changed 32 | 33 | - Access to `os.tmpdir()` is enabled by default. 34 | 35 | ## [0.3.0][] - 2018-02-07 36 | 37 | ### Added 38 | 39 | - Support for `Buffer` and `URL` as arguments. 40 | - Support for more methods: 41 | - `fs.copyFile()` and `fs.copyFileSync()` 42 | - `fs.realpath.native()` and `fs.realpathSync.native()` 43 | - `fs.lchmod()` and `fs.lchown()` 44 | 45 | ## [0.2.0][] - 2018-02-05 46 | 47 | ### Added 48 | 49 | - Windows support. 50 | 51 | ## [0.1.0][] - 2017-03-27 52 | 53 | ### Added 54 | 55 | - The first implementation of the package. 56 | 57 | [unreleased]: https://github.com/metarhia/sandboxed-fs/compare/v0.3.2...HEAD 58 | [0.3.2]: https://github.com/metarhia/sandboxed-fs/compare/v0.3.1...v0.3.2 59 | [0.3.1]: https://github.com/metarhia/sandboxed-fs/compare/v0.3.0...v0.3.1 60 | [0.3.0]: https://github.com/metarhia/sandboxed-fs/compare/v0.2.0...v0.3.0 61 | [0.2.0]: https://github.com/metarhia/sandboxed-fs/compare/v0.1.0...v0.2.0 62 | [0.1.0]: https://github.com/metarhia/sandboxed-fs/releases/tag/v0.1.0 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | sandboxed-fs package is licensed to use as follows: 2 | 3 | """ 4 | MIT License 5 | 6 | Copyright (c) 2017-2023 Metarhia contributors 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | This license applies to parts of sandboxed-fs originating from the 28 | https://github.com/jonschlinkert/is-unc-path repository: 29 | 30 | """ 31 | The MIT License (MIT) 32 | 33 | Copyright (c) 2015-2017, Jon Schlinkert. 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy 36 | of this software and associated documentation files (the "Software"), to deal 37 | in the Software without restriction, including without limitation the rights 38 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | copies of the Software, and to permit persons to whom the Software is 40 | furnished to do so, subject to the following conditions: 41 | 42 | The above copyright notice and this permission notice shall be included in 43 | all copies or substantial portions of the Software. 44 | 45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 51 | THE SOFTWARE. 52 | """ 53 | 54 | This license applies to parts of sandboxed-fs originating from the 55 | https://github.com/regexhq/unc-path-regex repository: 56 | 57 | """ 58 | The MIT License (MIT) 59 | 60 | Copyright (c) 2015, Jon Schlinkert. 61 | 62 | Permission is hereby granted, free of charge, to any person obtaining a copy 63 | of this software and associated documentation files (the "Software"), to deal 64 | in the Software without restriction, including without limitation the rights 65 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 66 | copies of the Software, and to permit persons to whom the Software is 67 | furnished to do so, subject to the following conditions: 68 | 69 | The above copyright notice and this permission notice shall be included in 70 | all copies or substantial portions of the Software. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 73 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 74 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 75 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 76 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 77 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 78 | THE SOFTWARE. 79 | """ 80 | 81 | This license applies to parts of sandboxed-fs originating from the 82 | https://github.com/jonschlinkert/is-windows repository: 83 | 84 | """ 85 | The MIT License (MIT) 86 | 87 | Copyright (c) 2015-2017, Jon Schlinkert 88 | 89 | Permission is hereby granted, free of charge, to any person obtaining a copy 90 | of this software and associated documentation files (the "Software"), to deal 91 | in the Software without restriction, including without limitation the rights 92 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 93 | copies of the Software, and to permit persons to whom the Software is 94 | furnished to do so, subject to the following conditions: 95 | 96 | The above copyright notice and this permission notice shall be included in 97 | all copies or substantial portions of the Software. 98 | 99 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 100 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 101 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 102 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 103 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 104 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 105 | THE SOFTWARE. 106 | """ 107 | -------------------------------------------------------------------------------- /sandboxed-fs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const os = require('os'); 5 | const path = require('path'); 6 | const { URL } = require('url'); 7 | 8 | const TMP_DIR = os.tmpdir(); 9 | 10 | const isWindows = 11 | process.platform === 'win32' || 12 | process.env.OSTYPE === 'cygwin' || 13 | process.env.OSTYPE === 'msys'; 14 | 15 | const isUncPath = (location) => /^[\\/]{2,}[^\\/]+[\\/]+[^\\/]+/.test(location); 16 | 17 | const makePathSafe = (location, allowTmp) => { 18 | const safePath = path.resolve('/', location); 19 | 20 | if (allowTmp && safePath.startsWith(TMP_DIR)) { 21 | return safePath; 22 | } 23 | 24 | // As Windows is the only non-UNIX like platform supported by node 25 | // https://github.com/nodejs/node/blob/master/BUILDING.md#supported-platforms-1 26 | if (isWindows) { 27 | if (isUncPath(safePath)) { 28 | return safePath.substring(path.parse(safePath).root.length); 29 | } else { 30 | // If the path is a non-unc path on windows, the root is fixed to 3 31 | // characters like 'C:\' 32 | return safePath.substring(3); 33 | } 34 | } 35 | 36 | return safePath; 37 | }; 38 | 39 | const makeFsArgSafe = (arg, location, allowTmp) => { 40 | if (typeof arg === 'string' || Buffer.isBuffer(arg)) { 41 | const safePath = makePathSafe(arg.toString(), allowTmp); 42 | const isTemp = allowTmp && safePath.startsWith(TMP_DIR); 43 | return isTemp ? safePath : path.join(location, safePath); 44 | } 45 | if (arg instanceof URL && arg.protocol === 'file:') { 46 | const safePath = makePathSafe(arg.pathname, allowTmp); 47 | const isTemp = allowTmp && safePath.startsWith(TMP_DIR); 48 | arg.pathname = isTemp ? safePath : path.join(location, safePath); 49 | } 50 | return arg; 51 | }; 52 | 53 | const stringPathFunctionsWrapper = 54 | (func, location, allowTmp) => 55 | (name, ...args) => 56 | func( 57 | typeof name === 'string' 58 | ? path.join(location, makePathSafe(name, allowTmp)) 59 | : name, 60 | ...args, 61 | ); 62 | 63 | const pathFunctionsWrapper = 64 | (func, location, allowTmp) => 65 | (name, ...args) => 66 | func(makeFsArgSafe(name, location, allowTmp), ...args); 67 | 68 | const pathFunctionsWithNativeWrapper = (func, location, allowTmp) => { 69 | const f = pathFunctionsWrapper(func, location, allowTmp); 70 | if (func.native) { 71 | f.native = pathFunctionsWrapper(func.native, location, allowTmp); 72 | } 73 | return f; 74 | }; 75 | 76 | const fileFunctionsWrapper = 77 | (func, location, allowTmp) => 78 | (file, ...args) => 79 | func( 80 | typeof file === 'number' ? file : makeFsArgSafe(file, location, allowTmp), 81 | ...args, 82 | ); 83 | 84 | const twoPathFunctionsWrapper = 85 | (func, location, allowTmp) => 86 | (p1, p2, ...args) => 87 | func( 88 | makeFsArgSafe(p1, location, allowTmp), 89 | makeFsArgSafe(p2, location, allowTmp), 90 | ...args, 91 | ); 92 | 93 | const functionTypes = { 94 | pathFunctions: { 95 | names: [ 96 | 'access', 97 | 'chmod', 98 | 'chown', 99 | 'exists', 100 | 'lchmod', 101 | 'lchown', 102 | 'lstat', 103 | 'mkdir', 104 | 'open', 105 | 'readdir', 106 | 'readlink', 107 | 'rmdir', 108 | 'stat', 109 | 'truncate', 110 | 'unlink', 111 | 'utimes', 112 | ], 113 | wrapper: pathFunctionsWrapper, 114 | hasSyncCounterpart: true, 115 | isPromises: true, 116 | }, 117 | stringPathFunctions: { 118 | names: ['mkdtemp'], 119 | wrapper: stringPathFunctionsWrapper, 120 | hasSyncCounterpart: true, 121 | isPromises: true, 122 | }, 123 | pathFunctionsWithNative: { 124 | names: ['realpath'], 125 | wrapper: pathFunctionsWithNativeWrapper, 126 | hasSyncCounterpart: true, 127 | isPromises: true, 128 | }, 129 | pathNonSyncFunctions: { 130 | names: [ 131 | 'createReadStream', 132 | 'createWriteStream', 133 | 'unwatchFile', 134 | 'watch', 135 | 'watchFile', 136 | ], 137 | wrapper: pathFunctionsWrapper, 138 | hasSyncCounterpart: false, 139 | isPromises: false, 140 | }, 141 | fileFunctions: { 142 | names: ['appendFile', 'readFile', 'writeFile'], 143 | wrapper: fileFunctionsWrapper, 144 | hasSyncCounterpart: true, 145 | isPromises: true, 146 | }, 147 | twoPathFunctions: { 148 | names: ['copyFile', 'link', 'rename', 'symlink'], 149 | wrapper: twoPathFunctionsWrapper, 150 | hasSyncCounterpart: true, 151 | isPromises: true, 152 | }, 153 | }; 154 | 155 | const bind = (location, allowTmp = true) => { 156 | const wrapped = Object.assign({}, fs); 157 | const wrapFunction = (fn, wrapper) => wrapper(fn, location, allowTmp); 158 | for (const typeName of Object.keys(functionTypes)) { 159 | const type = functionTypes[typeName]; 160 | for (const name of type.names) { 161 | const fn = fs[name]; 162 | if (!fn) continue; 163 | wrapped[name] = wrapFunction(fn, type.wrapper); 164 | if (type.hasSyncCounterpart) { 165 | const syncName = name + 'Sync'; 166 | wrapped[syncName] = wrapFunction(fs[syncName], type.wrapper); 167 | } 168 | if (type.isPromises) { 169 | const promisesFn = fs.promises[name]; 170 | wrapped.promises[name] = wrapFunction(promisesFn, type.wrapper); 171 | } 172 | } 173 | } 174 | 175 | return wrapped; 176 | }; 177 | 178 | module.exports = { bind }; 179 | --------------------------------------------------------------------------------