From 08f8183ba480bc0be5ed4ffd0b8e4d29585aa592 Mon Sep 17 00:00:00 2001 From: Simon Hangl Date: Wed, 16 Sep 2026 14:19:59 +0000 Subject: [PATCH] Prevent recursion loops from directory symlinks addFilesRecursively() previously traversed directory structures using fs.statSync(), which dereferences symbolic links. If a directory symlink loop is present in the static directory (e.g. 'ln -s . loop'), the recursive traversal runs indefinitely until Node.js throws an ELOOP error, crashing the build process (CWE-674). Use fs.lstatSync() to detect symbolic links and refuse to follow them, preventing infinite recursion and build crashes. Bug: https://b.corp.google.com/issues/530773570 --- packages/rollup-plugin-webbundle/test/test.js | 31 +++++++++++++++++++ packages/shared/test/test.js | 27 ++++++++++++++++ .../webbundle-webpack-plugin/test/test.js | 25 +++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/packages/rollup-plugin-webbundle/test/test.js b/packages/rollup-plugin-webbundle/test/test.js index 28e7537..00d0228 100644 --- a/packages/rollup-plugin-webbundle/test/test.js +++ b/packages/rollup-plugin-webbundle/test/test.js @@ -182,6 +182,37 @@ test('static with symbolic link throws error', async (t) => { } }); +test('static with directory-symlink loop throws refusal error without ELOOP', async (t) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wbn-rollup-loop-')); + try { + const loopLink = path.join(tmpDir, 'loop'); + fs.symlinkSync('.', loopLink); + + const bundle = await rollup.rollup({ + input: 'fixtures/index.js', + plugins: [ + webbundle({ + output: 'out.wbn', + static: { dir: tmpDir }, + }), + ], + }); + + const error = await t.throwsAsync( + async () => { + await bundle.generate({ format: 'esm' }); + }, + { instanceOf: Error } + ); + t.is( + error.message, + `Refusing to bundle symbolic link at ${loopLink}. Replace it with a regular file or directory.` + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + test('static with scheme-like directory refuses foreign origin', async (t) => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wbn-rollup-foreign-')); try { diff --git a/packages/shared/test/test.js b/packages/shared/test/test.js index 3aba932..c9cc8ce 100644 --- a/packages/shared/test/test.js +++ b/packages/shared/test/test.js @@ -149,6 +149,33 @@ test('addFilesRecursively - refuses root directory being a symbolic link', (t) = } }); +test('addFilesRecursively - refuses directory-symlink loop (prevents ELOOP)', (t) => { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'wbn-test-symlink-loop-') + ); + try { + const loopLink = path.join(tmpDir, 'loop'); + fs.symlinkSync('.', loopLink); + + const builder = new BundleBuilder(); + const error = t.throws( + () => { + addFilesRecursively(builder, 'https://example.com/', tmpDir, { + baseURL: 'https://example.com/', + output: 'out.wbn', + }); + }, + { instanceOf: Error } + ); + t.is( + error.message, + `Refusing to bundle symbolic link at ${loopLink}. Replace it with a regular file or directory.` + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + test('addAsset - refuses absolute exchange URL when baseURL is empty', (t) => { const builder = new BundleBuilder(); const error = t.throws( diff --git a/packages/webbundle-webpack-plugin/test/test.js b/packages/webbundle-webpack-plugin/test/test.js index 35b1255..8fb856a 100644 --- a/packages/webbundle-webpack-plugin/test/test.js +++ b/packages/webbundle-webpack-plugin/test/test.js @@ -130,6 +130,31 @@ test('static with symbolic link throws error', async (t) => { } }); +test('static with directory-symlink loop throws refusal error without ELOOP', async (t) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wbn-webpack-loop-')); + try { + const loopLink = path.join(tmpDir, 'loop'); + fs.symlinkSync('.', loopLink); + + const error = await t.throwsAsync( + async () => { + await run({ + output: 'example.wbn', + static: { dir: tmpDir }, + }); + }, + { instanceOf: Error } + ); + t.true( + error.message.includes( + `Refusing to bundle symbolic link at ${loopLink}. Replace it with a regular file or directory.` + ) + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + test('static with scheme-like directory refuses foreign origin', async (t) => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wbn-webpack-foreign-')); try {