diff --git a/packages/rollup-plugin-webbundle/test/test.js b/packages/rollup-plugin-webbundle/test/test.js index ff36596..92d593b 100644 --- a/packages/rollup-plugin-webbundle/test/test.js +++ b/packages/rollup-plugin-webbundle/test/test.js @@ -15,6 +15,8 @@ */ import test from 'ava'; +import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as rollup from 'rollup'; import crypto from 'crypto'; @@ -146,6 +148,71 @@ test('static', async (t) => { t.snapshot(parseWebBundle(output[keys[0]].source)); }); +test('static with symbolic link throws error', async (t) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wbn-rollup-symlink-')); + try { + const targetFile = path.join(tmpDir, 'target.txt'); + fs.writeFileSync(targetFile, 'sensitive data'); + const symlinkFile = path.join(tmpDir, 'symlink.txt'); + fs.symlinkSync(targetFile, symlinkFile); + + const bundle = await rollup.rollup({ + input: 'fixtures/index.js', + plugins: [ + webbundle({ + baseURL: 'https://wbn.example.com/', + 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 ${symlinkFile}. Replace it with a regular file or directory.` + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + +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('relative', async (t) => { const bundle = await rollup.rollup({ input: 'fixtures/index.js', diff --git a/packages/shared/test/test.js b/packages/shared/test/test.js index 2b6c2d8..864553f 100644 --- a/packages/shared/test/test.js +++ b/packages/shared/test/test.js @@ -15,7 +15,12 @@ */ import test from 'ava'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { BundleBuilder } from 'wbn'; import { getValidatedOptionsWithDefaults } from '../lib/types.js'; +import { addFilesRecursively } from '../lib/utils.js'; import * as wbnSign from 'wbn-sign'; const TEST_ED25519_PRIVATE_KEY = wbnSign.parsePemKey( @@ -50,3 +55,92 @@ test('headerOverride - IWA with bad headers', async (t) => { } } }); + +test('addFilesRecursively - refuses symbolic link to file', (t) => { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'wbn-test-symlink-file-') + ); + try { + const targetFile = path.join(tmpDir, 'target.txt'); + fs.writeFileSync(targetFile, 'sensitive data'); + const symlinkFile = path.join(tmpDir, 'symlink.txt'); + fs.symlinkSync(targetFile, symlinkFile); + + 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 ${symlinkFile}. Replace it with a regular file or directory.` + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + +test('addFilesRecursively - refuses symbolic link to directory', (t) => { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'wbn-test-symlink-dir-') + ); + try { + const targetDir = path.join(tmpDir, 'target-dir'); + fs.mkdirSync(targetDir); + fs.writeFileSync(path.join(targetDir, 'inner.txt'), 'sensitive data'); + const symlinkDir = path.join(tmpDir, 'symlink-dir'); + fs.symlinkSync(targetDir, symlinkDir); + + 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 ${symlinkDir}. Replace it with a regular file or directory.` + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + +test('addFilesRecursively - refuses root directory being a symbolic link', (t) => { + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'wbn-test-root-symlink-') + ); + try { + const targetDir = path.join(tmpDir, 'target-dir'); + fs.mkdirSync(targetDir); + fs.writeFileSync(path.join(targetDir, 'inner.txt'), 'some data'); + const symlinkDir = path.join(tmpDir, 'symlink-root'); + fs.symlinkSync(targetDir, symlinkDir); + + const builder = new BundleBuilder(); + const error = t.throws( + () => { + addFilesRecursively(builder, 'https://example.com/', symlinkDir, { + baseURL: 'https://example.com/', + output: 'out.wbn', + }); + }, + { instanceOf: Error } + ); + t.is( + error.message, + `Refusing to bundle symbolic link at ${symlinkDir}. Replace it with a regular file or directory.` + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); diff --git a/packages/shared/utils.ts b/packages/shared/utils.ts index 42601d0..cfeb957 100644 --- a/packages/shared/utils.ts +++ b/packages/shared/utils.ts @@ -19,8 +19,8 @@ import * as path from 'path'; import mime from 'mime'; import { combineHeadersForUrl, BundleBuilder } from 'wbn'; import { IntegrityBlockSigner } from 'wbn-sign'; -import { checkAndAddIwaHeaders } from './iwa-headers'; -import { ValidIbSignPluginOptions, ValidPluginOptions } from './types'; +import { checkAndAddIwaHeaders } from './iwa-headers.js'; +import { ValidIbSignPluginOptions, ValidPluginOptions } from './types.js'; // If the file name is 'index.html', create an entry for both baseURL/dir/ and // baseURL/dir/index.html which redirects to the aforementioned. Otherwise just @@ -84,13 +84,29 @@ export function addFilesRecursively( pluginOptions: ValidPluginOptions, recPath = '' ) { + if (recPath === '' && fs.lstatSync(dir).isSymbolicLink()) { + throw new Error( + `Refusing to bundle symbolic link at ${dir}. ` + + `Replace it with a regular file or directory.` + ); + } + const files = fs.readdirSync(dir); files.sort(); // Sort entries for reproducibility. for (const fileName of files) { const filePath = path.join(dir, fileName); - if (fs.statSync(filePath).isDirectory()) { + const stat = fs.lstatSync(filePath); + if (stat.isSymbolicLink()) { + // Refuse to follow symlinks so that files outside the static directory + // (credentials, SSH keys, etc.) cannot be pulled into the bundle. + throw new Error( + `Refusing to bundle symbolic link at ${filePath}. ` + + `Replace it with a regular file or directory.` + ); + } + if (stat.isDirectory()) { addFilesRecursively( builder, baseURL, diff --git a/packages/webbundle-webpack-plugin/test/test.js b/packages/webbundle-webpack-plugin/test/test.js index 36569a6..8c66870 100644 --- a/packages/webbundle-webpack-plugin/test/test.js +++ b/packages/webbundle-webpack-plugin/test/test.js @@ -20,6 +20,7 @@ import webpack from 'webpack'; import MemoryFS from 'memory-fs'; import url from 'url'; import fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import * as wbn from 'wbn'; import * as wbnSign from 'wbn-sign'; @@ -101,6 +102,34 @@ test('static', async (t) => { t.is(new TextDecoder('utf-8').decode(resp.body), html.toString()); }); +test('static with symbolic link throws error', async (t) => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wbn-webpack-symlink-')); + try { + const targetFile = path.join(tmpDir, 'target.txt'); + fs.writeFileSync(targetFile, 'sensitive data'); + const symlinkFile = path.join(tmpDir, 'symlink.txt'); + fs.symlinkSync(targetFile, symlinkFile); + + const error = await t.throwsAsync( + async () => { + await run({ + baseURL: 'https://example.com/', + static: { dir: tmpDir }, + output: 'example.wbn', + }); + }, + { instanceOf: Error } + ); + t.true( + error.message.includes( + `Refusing to bundle symbolic link at ${symlinkFile}. Replace it with a regular file or directory.` + ) + ); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); + test('relative', async (t) => { const { memfs } = await run({ static: { dir: path.join(__dirname, 'fixtures', 'static') },