Skip to content

Commit 512ba79

Browse files
authored
fix(storage): resolve Node compatibility crashes, security vulnerability, and stream hangs (#8622)
* fix(deps): update yargs to v17.7.2 for Node 18 and 26 compatibility * fix: relocate yargs shim generation to local directory and handle stream error callbacks in File.ts * fix: update safeFs reference in tests and add yargs shim generation with cleanup on process exit * fix: update fs mocking to use property descriptors for safer member access * refactor: simplify yargs resolution by targeting the build index directly instead of using shim files * refactor: generate unique local shim files for yargs resolution to improve security and prevent path conflicts
1 parent 40eeac1 commit 512ba79

4 files changed

Lines changed: 75 additions & 5 deletions

File tree

handwritten/storage/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"samples-test": "npm link && cd samples/ && npm link ../ && npm test && cd ../",
7272
"system-test:esm": "mocha build/esm/system-test --timeout 600000 --exit",
7373
"system-test": "mocha build/cjs/system-test --timeout 600000 --exit",
74-
"test": "cross-env NODE_OPTIONS='--no-deprecation' c8 mocha build/cjs/test"
74+
"test": "cross-env NODE_OPTIONS=\"--require ./scripts/preload-yargs.cjs --no-deprecation\" c8 mocha build/cjs/test"
7575
},
7676
"dependencies": {
7777
"@google-cloud/paginator": "^5.0.0",
@@ -106,7 +106,7 @@
106106
"@types/request": "^2.48.4",
107107
"@types/sinon": "^17.0.0",
108108
"@types/tmp": "0.2.6",
109-
"@types/yargs": "^17.0.10",
109+
"@types/yargs": "^17.0.35",
110110
"c8": "^9.0.0",
111111
"form-data": "^4.0.4",
112112
"gapic-tools": "^0.4.0",
@@ -125,7 +125,7 @@
125125
"path-to-regexp": "6.3.0",
126126
"tmp": "^0.2.0",
127127
"typescript": "^5.1.6",
128-
"yargs": "^17.3.1",
128+
"yargs": "^17.7.2",
129129
"cross-env": "^7.0.3"
130130
},
131131
"homepage": "https://github.com/googleapis/google-cloud-node/tree/main/handwritten/storage"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const Module = require('module');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const originalResolveFilename = Module._resolveFilename;
6+
7+
Module._resolveFilename = function(request, parent, isMain, options) {
8+
if (request === 'yargs/yargs') {
9+
const resolved = originalResolveFilename.apply(this, arguments);
10+
if (resolved.endsWith('.mjs')) {
11+
return resolved;
12+
}
13+
// Create a unique shim file in the local scripts directory to avoid shared temp directory vulnerabilities
14+
const safeHash = Buffer.from(resolved).toString('base64').replace(/[^a-zA-Z0-9]/g, '');
15+
const shimPath = path.join(__dirname, `yargs-shim-${safeHash}.cjs`);
16+
17+
if (!fs.existsSync(shimPath)) {
18+
const content = fs.readFileSync(resolved, 'utf8');
19+
// Replace `./build/index.cjs` with the absolute path
20+
const buildIndexPath = path.join(path.dirname(resolved), 'build', 'index.cjs');
21+
// We must replace ALL relative requires. Luckily yargs/yargs only requires `./build/index.cjs`
22+
const newContent = content.replace(/require\(['"]\.\/build\/index\.cjs['"]\)/g, `require(${JSON.stringify(buildIndexPath)})`);
23+
fs.writeFileSync(shimPath, newContent);
24+
}
25+
26+
global.__yargsShimCleanups = global.__yargsShimCleanups || new Set();
27+
if (!global.__yargsShimCleanups.has(shimPath)) {
28+
global.__yargsShimCleanups.add(shimPath);
29+
process.on('exit', () => {
30+
try {
31+
fs.unlinkSync(shimPath);
32+
} catch (e) {}
33+
});
34+
}
35+
36+
return shimPath;
37+
}
38+
return originalResolveFilename.apply(this, arguments);
39+
};

handwritten/storage/src/file.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2187,6 +2187,29 @@ class File extends ServiceObject<File, FileMetadata> {
21872187
// remove temporary noop listener as we now create a pipeline that handles the errors
21882188
emitStream.removeListener('error', noop);
21892189

2190+
if (fileWriteStream.destroyed) {
2191+
let callbackCalled = false;
2192+
const onError = (err: Error) => {
2193+
if (!callbackCalled) {
2194+
callbackCalled = true;
2195+
pipelineCallback(err);
2196+
}
2197+
};
2198+
fileWriteStream.once('error', onError);
2199+
emitStream.destroy();
2200+
2201+
process.nextTick(() => {
2202+
fileWriteStream.removeListener('error', onError);
2203+
if (!callbackCalled) {
2204+
callbackCalled = true;
2205+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2206+
const err = (fileWriteStream as any).errored || new Error('Write stream destroyed');
2207+
pipelineCallback(err);
2208+
}
2209+
});
2210+
return;
2211+
}
2212+
21902213
pipeline(
21912214
emitStream,
21922215
...(transformStreams as [Transform]),

handwritten/storage/test/file.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,15 @@ const fakePromisify = {
118118
};
119119

120120
const fsCached = fs;
121-
const fakeFs = {...fsCached};
121+
const safeFs: any = {};
122+
const descriptors = Object.getOwnPropertyDescriptors(fsCached);
123+
for (const key of Object.keys(descriptors)) {
124+
const desc = descriptors[key];
125+
if (desc && !desc.get) {
126+
Object.defineProperty(safeFs, key, desc);
127+
}
128+
}
129+
const fakeFs = {...safeFs} as typeof fs;
122130

123131
const zlibCached = zlib;
124132
let createGunzipOverride: Function | null;
@@ -223,7 +231,7 @@ describe('File', () => {
223231
});
224232

225233
beforeEach(() => {
226-
Object.assign(fakeFs, fsCached);
234+
Object.assign(fakeFs, safeFs);
227235
Object.assign(fakeOs, osCached);
228236
// eslint-disable-next-line @typescript-eslint/no-explicit-any
229237
FakeServiceObject.prototype.request = util.noop as any;

0 commit comments

Comments
 (0)