From 9912cb27719fbbf7b505d04d2e43c732cc2a5ae3 Mon Sep 17 00:00:00 2001 From: ankita-srivastava009 <125461220+ankita-srivastava009@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:52:29 +0100 Subject: [PATCH 1/2] Update package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 459ce3c0c..c828f3d1a 100644 --- a/package.json +++ b/package.json @@ -101,3 +101,4 @@ }, "packageManager": "yarn@4.14.1" } +//*** From b5f8c5999a8073b8fa66a4dc688b8425a48ce9c3 Mon Sep 17 00:00:00 2001 From: Ankita Srivastava Date: Thu, 30 Apr 2026 15:09:12 +0100 Subject: [PATCH 2/2] ankita-srivastava009-patch-1 --- app/util/secure-file-reader.js | 68 ++++++++++++++++++++++++++++ package.json | 11 +++-- server.js | 6 +-- test/util/secure-file-reader.spec.js | 58 ++++++++++++++++++++++++ yarn.lock | 33 +++++--------- 5 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 app/util/secure-file-reader.js create mode 100644 test/util/secure-file-reader.spec.js diff --git a/app/util/secure-file-reader.js b/app/util/secure-file-reader.js new file mode 100644 index 000000000..e1916941c --- /dev/null +++ b/app/util/secure-file-reader.js @@ -0,0 +1,68 @@ +const fs = require('fs'); +const path = require('path'); + +const containsPath = (basePath, targetPath) => { + const relativePath = path.relative(basePath, targetPath); + + return relativePath === '' || ( + !relativePath.startsWith('..') && + !path.isAbsolute(relativePath) + ); +}; + +const resolveContainedFile = (baseDirectory, fileName) => { + if (path.isAbsolute(fileName)) { + throw new Error('File path must be relative to the configured directory'); + } + + const resolvedBaseDirectory = path.resolve(baseDirectory); + const baseStats = fs.lstatSync(resolvedBaseDirectory); + + if (baseStats.isSymbolicLink() || !baseStats.isDirectory()) { + throw new Error('Configured directory must be a real directory'); + } + + const resolvedFilePath = path.resolve(resolvedBaseDirectory, fileName); + + if (!containsPath(resolvedBaseDirectory, resolvedFilePath)) { + throw new Error('File path must stay within the configured directory'); + } + + const fileStats = fs.lstatSync(resolvedFilePath); + + if (fileStats.isSymbolicLink() || !fileStats.isFile()) { + throw new Error('Configured file must be a regular file'); + } + + const realBaseDirectory = fs.realpathSync(resolvedBaseDirectory); + const realFilePath = fs.realpathSync(resolvedFilePath); + + if (!containsPath(realBaseDirectory, realFilePath)) { + throw new Error('File path must resolve within the configured directory'); + } + + return realFilePath; +}; + +const readContainedFile = (baseDirectory, fileName) => { + const filePath = resolveContainedFile(baseDirectory, fileName); + const noFollowFlag = fs.constants.O_NOFOLLOW || 0; + const fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollowFlag); + + try { + const fileStats = fs.fstatSync(fd); + + if (!fileStats.isFile()) { + throw new Error('Configured file must be a regular file'); + } + + return fs.readFileSync(fd); + } finally { + fs.closeSync(fd); + } +}; + +module.exports = { + readContainedFile, + resolveContainedFile +}; diff --git a/package.json b/package.json index c828f3d1a..235e79989 100644 --- a/package.json +++ b/package.json @@ -94,11 +94,16 @@ "cross-spawn": "^7.0.6", "braces": "^3.0.3", "brace-expansion": "^1.1.12", - "picomatch": "^2.3.2" + "basic-ftp": "^5.3.1", + "picomatch": "^2.3.2", + "tar-fs": "^3.1.1", + "tmp": "^0.2.5" }, "overrides": { - "picomatch": "^2.3.2" + "basic-ftp": "^5.3.1", + "picomatch": "^2.3.2", + "tar-fs": "^3.1.1", + "tmp": "^0.2.5" }, "packageManager": "yarn@4.14.1" } -//*** diff --git a/server.js b/server.js index fec709390..1d08d1ef7 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,7 @@ let debug = require('debug')('ccd-api-gateway-web:server'); let http = require('http'); let https = require('https'); let path = require('path'); -let fs = require('fs'); +const {readContainedFile} = require('./app/util/secure-file-reader'); /** * Get port from environment and store in Express. @@ -30,8 +30,8 @@ function createServer(app) { if (process.env.ENV === 'localdev') { const sslDirectory = path.join(__dirname, '..', 'app', 'resources', 'localhost-ssl'); const sslOptions = { - cert: fs.readFileSync(path.join(sslDirectory, 'localhost.crt')), - key: fs.readFileSync(path.join(sslDirectory, 'localhost.key')), + cert: readContainedFile(sslDirectory, 'localhost.crt'), + key: readContainedFile(sslDirectory, 'localhost.key'), secureProtocol: 'TLS_method' }; return https.createServer(sslOptions, app); diff --git a/test/util/secure-file-reader.spec.js b/test/util/secure-file-reader.spec.js new file mode 100644 index 000000000..b5ffe3f08 --- /dev/null +++ b/test/util/secure-file-reader.spec.js @@ -0,0 +1,58 @@ +const chai = require('chai'); +const expect = chai.expect; +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const secureFileReader = require('../../app/util/secure-file-reader'); + +describe('secure file reader', () => { + let tempDirectory; + let outsideDirectory; + + beforeEach(() => { + tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'secure-file-reader-')); + outsideDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'secure-file-reader-outside-')); + }); + + afterEach(() => { + fs.rmSync(tempDirectory, {recursive: true, force: true}); + fs.rmSync(outsideDirectory, {recursive: true, force: true}); + }); + + it('should read files contained by the configured directory', () => { + fs.writeFileSync(path.join(tempDirectory, 'allowed.txt'), 'safe'); + + const content = secureFileReader.readContainedFile(tempDirectory, 'allowed.txt'); + + expect(content.toString()).to.equal('safe'); + }); + + it('should reject path traversal outside the configured directory', () => { + fs.writeFileSync(path.join(outsideDirectory, 'outside.txt'), 'outside'); + const traversalPath = path.join('..', path.basename(outsideDirectory), 'outside.txt'); + + expect(() => secureFileReader.readContainedFile(tempDirectory, traversalPath)) + .to.throw('File path must stay within the configured directory'); + }); + + it('should reject symbolic links', function () { + const targetPath = path.join(outsideDirectory, 'outside.txt'); + const linkPath = path.join(tempDirectory, 'linked.txt'); + + fs.writeFileSync(targetPath, 'outside'); + + try { + fs.symlinkSync(targetPath, linkPath); + } catch (err) { + if (err.code === 'EPERM') { + this.skip(); + } + + throw err; + } + + expect(() => secureFileReader.readContainedFile(tempDirectory, 'linked.txt')) + .to.throw('Configured file must be a regular file'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 91bce974a..cc1693ee7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1889,10 +1889,10 @@ __metadata: languageName: node linkType: hard -"basic-ftp@npm:^5.0.2": - version: 5.0.5 - resolution: "basic-ftp@npm:5.0.5" - checksum: 10/3dc56b2092b10d67e84621f5b9bbb0430469499178e857869194184d46fbdd367a9aa9fad660084388744b074b5f540e6ac8c22c0826ebba4fcc86a9d1c324e2 +"basic-ftp@npm:^5.3.1": + version: 5.3.1 + resolution: "basic-ftp@npm:5.3.1" + checksum: 10/9232ee155114efafadf5adee86a6750208653a9071e53e9803dceac61a66b3ba3974771ff2490ff28f1a118fdfb806ffcc488f64609e61a9cedb52480b312843 languageName: node linkType: hard @@ -5014,13 +5014,6 @@ __metadata: languageName: node linkType: hard -"os-tmpdir@npm:~1.0.2": - version: 1.0.2 - resolution: "os-tmpdir@npm:1.0.2" - checksum: 10/5666560f7b9f10182548bf7013883265be33620b1c1b4a4d405c25be2636f970c5488ff3e6c48de75b55d02bde037249fe5dbfbb4c0fb7714953d56aed062e6d - languageName: node - linkType: hard - "otp@npm:^0.1.3": version: 0.1.3 resolution: "otp@npm:0.1.3" @@ -6254,9 +6247,9 @@ __metadata: languageName: node linkType: hard -"tar-fs@npm:^3.0.6": - version: 3.0.9 - resolution: "tar-fs@npm:3.0.9" +"tar-fs@npm:^3.1.1": + version: 3.1.2 + resolution: "tar-fs@npm:3.1.2" dependencies: bare-fs: "npm:^4.0.1" bare-path: "npm:^3.0.0" @@ -6267,7 +6260,7 @@ __metadata: optional: true bare-path: optional: true - checksum: 10/00e194ef36ced339000099c3cb5205fcd9636a531158d73e0fc1ca056fbcf8dcf39a398cbc71f030bbf938d4f477f47e2913c6e37e9c007bad31e0768a120590 + checksum: 10/b358fb7061eebb42bfa6f122cf62d1bdd40dc619117863f3b59eeaa4f880dc03707014905bdb592e77176703d9045956d1ba27adda4458805f9f7cbf62015cbd languageName: node linkType: hard @@ -6332,12 +6325,10 @@ __metadata: languageName: node linkType: hard -"tmp@npm:^0.0.33": - version: 0.0.33 - resolution: "tmp@npm:0.0.33" - dependencies: - os-tmpdir: "npm:~1.0.2" - checksum: 10/09c0abfd165cff29b32be42bc35e80b8c64727d97dedde6550022e88fa9fd39a084660415ed8e3ebaa2aca1ee142f86df8b31d4196d4f81c774a3a20fd4b6abf +"tmp@npm:^0.2.5": + version: 0.2.5 + resolution: "tmp@npm:0.2.5" + checksum: 10/dd4b78b32385eab4899d3ae296007b34482b035b6d73e1201c4a9aede40860e90997a1452c65a2d21aee73d53e93cd167d741c3db4015d90e63b6d568a93d7ec languageName: node linkType: hard