Rollup 4 has Arbitrary File Write via Path Traversal
Description
Rollup is a module bundler for JavaScript. Versions prior to 2.80.0, 3.30.0, and 4.59.0 of the Rollup module bundler (specifically v4.x and present in current source) is vulnerable to an Arbitrary File Write via Path Traversal. Insecure file name sanitization in the core engine allows an attacker to control output filenames (e.g., via CLI named inputs, manual chunk aliases, or malicious plugins) and use traversal sequences (../) to overwrite files anywhere on the host filesystem that the build process has permissions for. This can lead to persistent Remote Code Execution (RCE) by overwriting critical system or user configuration files. Versions 2.80.0, 3.30.0, and 4.59.0 contain a patch for the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
rollupnpm | < 2.80.0 | 2.80.0 |
rollupnpm | >= 3.0.0, < 3.30.0 | 3.30.0 |
rollupnpm | >= 4.0.0, < 4.59.0 | 4.59.0 |
Affected products
1Patches
3c60770d7aaf7Validate bundle stays within output dir (#6275)
36 files changed · +588 −183
audit-resolve.json+43 −58 modified@@ -1,104 +1,89 @@ { "decisions": { - "1113214": { - "decision": "ignore", - "madeAt": 1771566169457, - "expiresAt": 1774158145442 - }, - "1113296": { - "decision": "ignore", - "madeAt": 1771566180086, - "expiresAt": 1774158145442 - }, - "1113214|@eslint/js>eslint>@eslint-community/eslint-utils>ajv": { - "decision": "ignore", - "madeAt": 1771567906111, - "expiresAt": 1774159894900 - }, - "1113214|@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch>eslint>@eslint-community/eslint-utils>ajv": { + "1113296|@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch>@typescript-eslint/utils>@eslint-community/eslint-utils>eslint>ajv": { + "1113296|@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|eslint>@eslint-community/eslint-utils>ajv": { + "1113296|eslint-plugin-vue>@eslint-community/eslint-utils>eslint>ajv>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|eslint-config-prettier>eslint>@eslint-community/eslint-utils>ajv": { + "1113296|fixturify>matcher-collection>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|eslint-plugin-prettier>eslint>@eslint-community/eslint-utils>ajv": { + "1113296|mocha>glob>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|eslint-plugin-unicorn>@eslint-community/eslint-utils>eslint>ajv": { + "1113296|nyc>glob>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|eslint-plugin-vue>@eslint-community/eslint-utils>eslint>ajv": { + "1113296|typescript-eslint>@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|typescript-eslint>@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch>eslint>@eslint-community/eslint-utils>ajv": { + "1113296|wasm-pack>binary-install>rimraf>glob>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, + "madeAt": 1771567910429, "expiresAt": 1774159894900 }, - "1113214|vue-eslint-parser>eslint>@eslint-community/eslint-utils>ajv": { + "1113371|@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567906111, - "expiresAt": 1774159894900 + "madeAt": 1771655968819, + "expiresAt": 1774247954442 }, - "1113296|@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { + "1113371|eslint-plugin-vue>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 }, - "1113296|@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch": { + "1113371|fixturify>matcher-collection>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 }, - "1113296|eslint-plugin-vue>@eslint-community/eslint-utils>eslint>ajv>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { + "1113371|mocha>glob>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 }, - "1113296|fixturify>matcher-collection>minimatch": { + "1113371|nyc>glob>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 }, - "1113296|mocha>glob>minimatch": { + "1113371|typescript-eslint>@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 }, - "1113296|nyc>glob>minimatch": { + "1113371|wasm-pack>binary-install>rimraf>glob>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 }, - "1113296|typescript-eslint>@typescript-eslint/eslint-plugin>@typescript-eslint/parser>@typescript-eslint/typescript-estree>minimatch": { + "1113398|ajv": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656216790, + "expiresAt": 1774248208896 }, - "1113296|wasm-pack>binary-install>rimraf>glob>minimatch": { + "1113371|@typescript-eslint/type-utils>@typescript-eslint/typescript-estree>minimatch": { "decision": "ignore", - "madeAt": 1771567910429, - "expiresAt": 1774159894900 + "madeAt": 1771656226170, + "expiresAt": 1774248219706 } }, "rules": {},
browser/src/path.ts+35 −19 modified@@ -35,6 +35,31 @@ export function extname(path: string): string { return match ? match[0] : ''; } +export function join(...segments: string[]): string { + const joined = segments.join('/'); + const absolute = ANY_SLASH_REGEX.test(joined[0]); + return ( + (absolute ? '/' : '') + + (normalizePathSegments(joined.split(ANY_SLASH_REGEX), absolute) || (absolute ? '' : '.')) + ); +} + +function normalizePathSegments(parts: string[], absolute = false): string { + const normalized: string[] = []; + for (const part of parts) { + if (part === '..') { + if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') { + normalized.pop(); + } else if (!absolute) { + normalized.push('..'); + } + } else if (part !== '.' && part !== '') { + normalized.push(part); + } + } + return normalized.join('/'); +} + export function relative(from: string, to: string): string { const fromParts = from.split(ANY_SLASH_REGEX).filter(Boolean); const toParts = to.split(ANY_SLASH_REGEX).filter(Boolean); @@ -60,30 +85,21 @@ export function relative(from: string, to: string): string { } export function resolve(...paths: string[]): string { - const firstPathSegment = paths.shift(); - if (!firstPathSegment) { - return '/'; - } - let resolvedParts = firstPathSegment.split(ANY_SLASH_REGEX); - + let parts: string[] = []; + let isAbsoluteResult = false; for (const path of paths) { if (isAbsolute(path)) { - resolvedParts = path.split(ANY_SLASH_REGEX); + parts = path.split(ANY_SLASH_REGEX); + isAbsoluteResult = true; } else { - const parts = path.split(ANY_SLASH_REGEX); - - while (parts[0] === '.' || parts[0] === '..') { - const part = parts.shift(); - if (part === '..') { - resolvedParts.pop(); - } - } - - resolvedParts.push(...parts); + parts.push(...path.split(ANY_SLASH_REGEX)); } } - - return resolvedParts.join('/'); + const normalized = normalizePathSegments(parts, isAbsoluteResult); + if (!isAbsoluteResult) return normalized || '/'; + // Windows absolute paths (e.g. "C:/path") must not get a leading "/" prepended. + // Unix absolute paths must start with "/". + return /^[A-Za-z]:/.test(normalized) ? normalized : '/' + normalized; } // Used for running the browser build locally in Vite
.github/copilot-instructions.md+8 −2 modified@@ -36,18 +36,24 @@ When adding/modifying functions that cross the JS-Rust boundary: - Focus on performance, avoid unnecessary copying of data or AST loops, build the final buffer once - When adding headers, reserve space for them once upfront to avoid the need to change all existing buffer references +## Browser Path Shim + +- `browser/src/path.ts` replaces `node:path` in the browser build (wired via `rollup.config.ts` aliases) +- `src/utils/relativeId.ts` imports `relative` **directly** from `../../browser/src/path` (not via `src/utils/path.ts`) so it works in both builds +- `src/utils/path.ts` re-exports from `node:path`; for browser builds rollup.config.ts substitutes `browser/src/path.ts` transparently for all other imports + ## Development Workflow ### Build Outputs - **Node build**: Artifacts placed in `dist/` (JavaScript + `.node` native modules) -- **Browser build**: Artifacts placed in `browser/dist/` +- **Browser build**: Artifacts placed in `browser/dist/`; browser tests use `browser/dist/rollup.browser.js` - All tests import from these dist folders - tests run against the full built artifact only ### Quick Rebuild Commands - `npm run build:quick` - Rebuild both JavaScript and Rust for Node build, copy to dist/ -- `npm run update:js` - Rebuild only JavaScript for Node, copy native to dist/ +- `npm run update:js` - Rebuild only JavaScript for both Node (`dist/`) and browser (`browser/dist/`), then copy native to dist/; run this after any change to `src/` or `browser/src/` - `npm run update:napi` - Rebuild only Rust NAPI, copy to dist/ - `npm run build:copy-native` - Copy Rust `.node` files to dist/ (called internally by update commands)
package-lock.json+44 −102 modified@@ -272,6 +272,7 @@ "integrity": "sha512-uGv2P3lcviuaZy8ZOAyN60cZdhOVyjXwaDC27a1qdp3Pb5Azn+lLSJwkHU4TNRpphHmIei9HZuUxwQroujdPjw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.49.0", "@algolia/requester-browser-xhr": "5.49.0", @@ -427,6 +428,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -601,6 +603,7 @@ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", @@ -849,39 +852,13 @@ } } }, - "node_modules/@docsearch/js/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@docsearch/js/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "extraneous": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, "node_modules/@docsearch/js/node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "extraneous": true, + "dev": true, "license": "MIT", + "optional": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -3931,6 +3908,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -4111,7 +4089,6 @@ "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", @@ -5029,8 +5006,7 @@ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", @@ -5556,7 +5532,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@types/node": "*" } @@ -5606,6 +5581,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -6309,6 +6285,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6343,7 +6320,6 @@ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 14" } @@ -6395,6 +6371,7 @@ "integrity": "sha512-Tse7vx7WOvbU+kpq/L3BrBhSWTPbtMa59zIEhMn+Z2NoxZlpcCRUDCRxQ7kDFs1T3CHxDgvb+mDuILiBBpBaAA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.15.0", "@algolia/client-abtesting": "5.49.0", @@ -6558,7 +6535,6 @@ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.0.1" }, @@ -6613,7 +6589,6 @@ "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "react-native-b4a": "*" }, @@ -6639,7 +6614,6 @@ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6656,7 +6630,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -6683,7 +6656,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "bare": ">=1.14.0" } @@ -6695,7 +6667,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "bare-os": "^3.0.1" } @@ -6707,7 +6678,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "streamx": "^2.21.0", "teex": "^1.0.1" @@ -6732,7 +6702,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "bare-path": "^3.0.0" } @@ -6756,8 +6725,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", @@ -6778,7 +6746,6 @@ "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" } @@ -6889,6 +6856,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6928,6 +6896,7 @@ "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7037,7 +7006,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -7049,7 +7017,6 @@ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -7146,7 +7113,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -7279,6 +7245,7 @@ "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", @@ -7342,7 +7309,6 @@ "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" @@ -7880,7 +7846,6 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -7968,6 +7933,7 @@ "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -8425,6 +8391,7 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8532,7 +8499,6 @@ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 14" } @@ -8669,7 +8635,6 @@ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -8886,7 +8851,6 @@ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -8910,7 +8874,6 @@ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -9070,7 +9033,6 @@ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -9094,7 +9056,6 @@ "dev": true, "license": "BSD-3-Clause", "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9105,6 +9066,7 @@ "integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -9161,6 +9123,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9424,7 +9387,6 @@ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "bare-events": "^2.7.0" } @@ -9547,7 +9509,6 @@ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -9599,8 +9560,7 @@ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", @@ -9659,7 +9619,6 @@ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pend": "~1.2.0" } @@ -9876,6 +9835,7 @@ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -10090,7 +10050,6 @@ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pump": "^3.0.0" }, @@ -10107,7 +10066,6 @@ "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", @@ -10450,7 +10408,6 @@ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -10465,7 +10422,6 @@ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -10536,8 +10492,7 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.2", @@ -10573,7 +10528,6 @@ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -10591,7 +10545,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=4" } @@ -10675,7 +10628,6 @@ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 12" } @@ -10685,8 +10637,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -11124,6 +11075,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -11210,8 +11162,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -11710,8 +11661,9 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "extraneous": true, + "dev": true, "license": "MIT", + "optional": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -12183,6 +12135,7 @@ "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", @@ -13008,7 +12961,6 @@ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -13687,7 +13639,6 @@ "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", @@ -13708,7 +13659,6 @@ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" @@ -13773,7 +13723,6 @@ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -13787,7 +13736,6 @@ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -14031,8 +13979,7 @@ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", @@ -14286,6 +14233,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14470,6 +14418,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -14565,7 +14514,6 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4.0" } @@ -14587,7 +14535,6 @@ "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -14608,7 +14555,6 @@ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -14718,7 +14664,6 @@ "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", @@ -14796,6 +14741,7 @@ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14806,6 +14752,7 @@ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15219,6 +15166,7 @@ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -15584,7 +15532,6 @@ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -15606,7 +15553,6 @@ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" @@ -15622,7 +15568,6 @@ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -15855,7 +15800,6 @@ "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", @@ -16175,6 +16119,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -16257,7 +16202,6 @@ "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" @@ -16273,7 +16217,6 @@ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", @@ -16297,7 +16240,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "streamx": "^2.12.5" } @@ -16402,7 +16344,6 @@ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "b4a": "^1.6.4" } @@ -16462,8 +16403,7 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/time-zone": { "version": "2.0.0", @@ -16618,7 +16558,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/twoslash": { "version": "0.3.6", @@ -16697,8 +16638,7 @@ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/typedarray": { "version": "0.0.6", @@ -16723,6 +16663,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16785,7 +16726,6 @@ "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -17054,6 +16994,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -17515,6 +17456,7 @@ "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", @@ -17557,6 +17499,7 @@ "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", @@ -17591,6 +17534,7 @@ "integrity": "sha512-xj3YCvSLNDKt1iF9OcImWHhmYcihVu9p4b9s4PGR/qp6yhW+tZJaypGxHScRyOrdnHvaOeF+YkZOdKwbgGvp5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "3.2.4" @@ -17945,7 +17889,6 @@ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -17995,6 +17938,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -18140,7 +18084,6 @@ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -18178,7 +18121,6 @@ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" }
src/Bundle.ts+29 −1 modified@@ -22,12 +22,13 @@ import { logCannotAssignModuleToChunk, logChunkInvalid, logCircularChunk, + logFileNameOutsideOutputDirectory, logInvalidOption } from './utils/logs'; import type { OutputBundleWithPlaceholders } from './utils/outputBundle'; import { getOutputBundle, removeUnreferencedAssets } from './utils/outputBundle'; import { parseAst } from './utils/parseAst'; -import { isAbsolute } from './utils/path'; +import { isAbsolute, join } from './utils/path'; import type { PluginDriver } from './utils/PluginDriver'; import { renderChunks } from './utils/renderChunks'; import { timeEnd, timeStart } from './utils/timers'; @@ -99,6 +100,7 @@ export default class Bundle { isWrite ]); this.finaliseAssets(outputBundle); + validateOutputBundleFileNames(outputBundle); timeEnd('generate bundle', 2); timeEnd('GENERATE', 1); @@ -362,3 +364,29 @@ function addModuleToManualChunk( } manualChunkAliasByEntry.set(module, alias); } + +function isFileNameOutsideOutputDirectory(fileName: string): boolean { + // Use join() to normalize ".." segments, then replace backslashes so the + // string checks below work identically on Windows and POSIX. + const normalized = join(fileName).replaceAll('\\', '/'); + return ( + normalized === '..' || + normalized.startsWith('../') || + normalized === '.' || + isAbsolute(normalized) + ); +} + +function validateOutputBundleFileNames(bundle: OutputBundleWithPlaceholders): void { + for (const [bundleKey, entry] of Object.entries(bundle)) { + if (isFileNameOutsideOutputDirectory(bundleKey)) { + return error(logFileNameOutsideOutputDirectory(bundleKey)); + } + if (entry.type !== 'placeholder') { + const { fileName } = entry; + if (fileName !== bundleKey && isFileNameOutsideOutputDirectory(fileName)) { + return error(logFileNameOutsideOutputDirectory(fileName)); + } + } + } +}
src/utils/logs.ts+8 −0 modified@@ -133,6 +133,7 @@ const ADDON_ERROR = 'ADDON_ERROR', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FAIL_AFTER_WARNINGS = 'FAIL_AFTER_WARNINGS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', + FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FIRST_SIDE_EFFECT = 'FIRST_SIDE_EFFECT', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', @@ -437,6 +438,13 @@ export function logFileNameConflict(fileName: string): RollupLog { }; } +export function logFileNameOutsideOutputDirectory(fileName: string): RollupLog { + return { + code: FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY, + message: `The output file name "${fileName}" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.` + }; +} + export function logFileReferenceIdNotFoundForFilename(assetReferenceId: string): RollupLog { return { code: FILE_NOT_FOUND,
src/utils/path.ts+1 −1 modified@@ -15,4 +15,4 @@ export function normalize(path: string): string { return path.replace(BACKSLASH_REGEX, '/'); } -export { basename, dirname, extname, relative, resolve } from 'node:path'; +export { basename, dirname, extname, join, relative, resolve } from 'node:path';
test/browser/samples/error-file-name-absolute-path/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../testHelpers.js'); + +module.exports = defineTest({ + description: 'throws when a plugin adds an absolute file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['/etc/passwd'] = { + type: 'asset', + fileName: '/etc/passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "/etc/passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/browser/samples/error-file-name-path-traversal/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../testHelpers.js'); + +module.exports = defineTest({ + description: 'throws when a plugin adds a path traversal file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['a/../../escaped.js'] = { + type: 'asset', + fileName: 'a/../../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/../../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/browser/samples/error-file-name-windows-absolute-path/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../testHelpers.js'); + +module.exports = defineTest({ + description: 'throws when a plugin adds a Windows-style absolute file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['C:\\etc\\passwd'] = { + type: 'asset', + fileName: 'C:\\etc\\passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "C:\\etc\\passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/browser/samples/renormalizes-external-paths-past-root/_config.js+39 −0 added@@ -0,0 +1,39 @@ +const { join, dirname } = require('node:path').posix; + +module.exports = defineTest({ + description: + 'clamps at root when renormalizing external paths that traverse past the root via resolve', + options: { + input: '/main.js', + external(id) { + return id.endsWith('ext'); + }, + plugins: { + name: 'test-plugin', + resolveId(source, importer) { + if (source.endsWith('ext.js')) { + return false; + } + if (!importer) { + return source; + } + return join(dirname(importer), source); + }, + load(id) { + switch (id) { + case '/main.js': { + // dep.js is at root; the external is imported via '../../escaped-ext.js' + // from dep.js — two levels up from '/' — which should clamp to '/escaped-ext.js' + return `import './dep.js';`; + } + case '/dep.js': { + return `import '../../escaped-ext.js';`; + } + default: { + throw new Error(`Unexpected id ${id}`); + } + } + } + } + } +});
test/browser/samples/renormalizes-external-paths-past-root/_expected/main.js+1 −0 added@@ -0,0 +1 @@ +import './escaped-ext.js';
test/function/samples/error-file-name-absolute-path/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds an absolute file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['/etc/passwd'] = { + type: 'asset', + fileName: '/etc/passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "/etc/passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-absolute-path/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-deep-traversal/_config.js+27 −0 added@@ -0,0 +1,27 @@ +module.exports = defineTest({ + description: + 'throws when a file name resolves outside the output directory after normalizing deep ".." segments', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['a/b/../../../escape.js'] = { + type: 'asset', + fileName: 'a/b/../../../escape.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/b/../../../escape.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-deep-traversal/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-dot/_config.js+14 −0 added@@ -0,0 +1,14 @@ +module.exports = defineTest({ + description: 'throws when entryFileNames is "."', + options: { + output: { + entryFileNames: '.', + format: 'es' + } + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "." is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-dot-dot/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds ".." as a file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['..'] = { + type: 'asset', + fileName: '..', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name ".." is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-dot-dot/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-dot/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-escaped-via-filename-property/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin sets a safe bundle key but an escaping fileName property', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['safe.js'] = { + type: 'asset', + fileName: '../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-escaped-via-filename-property/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-path-traversal/_config.js+14 −0 added@@ -0,0 +1,14 @@ +module.exports = defineTest({ + description: 'throws when entryFileNames contains a mid-path traversal sequence', + options: { + output: { + entryFileNames: 'a/../../pwned.js', + format: 'es' + } + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/../../pwned.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-path-traversal/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-path-traversal-plugin/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds a path traversal file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['../escaped.js'] = { + type: 'asset', + fileName: '../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-path-traversal-plugin/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-windows-absolute-path/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds a Windows-style absolute file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['C:\\etc\\passwd'] = { + type: 'asset', + fileName: 'C:\\etc\\passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "C:\\etc\\passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-windows-absolute-path/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-double-dot-prefix-is-valid/_config.js+26 −0 added@@ -0,0 +1,26 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names that start with ".." but are not path traversal sequences', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['..foo.js'] = { + type: 'asset', + fileName: '..foo.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'content' + }; + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-double-dot-prefix-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-leading-dot-slash-is-valid/_config.js+26 −0 added@@ -0,0 +1,26 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names with a leading "./" prefix', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['./asset.txt'] = { + type: 'asset', + fileName: './asset.txt', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'content' + }; + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-leading-dot-slash-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-mid-path-up-then-down-is-valid/_config.js+22 −0 added@@ -0,0 +1,22 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names with ".." that normalize to a path inside the output directory', + options: { + plugins: [ + { + name: 'test', + buildStart() { + this.emitFile({ + type: 'asset', + fileName: 'foo/bar/../baz.js', + source: 'content' + }); + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-mid-path-up-then-down-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-subdirectory-is-valid/_config.js+22 −0 added@@ -0,0 +1,22 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names with subdirectories within the output directory', + options: { + plugins: [ + { + name: 'test', + buildStart() { + this.emitFile({ + type: 'asset', + fileName: 'sub/dir/asset.txt', + source: 'content' + }); + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-subdirectory-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
c8cf1f9c48c5Validate bundle stays within output dir (#6276)
34 files changed · +501 −21
browser/src/path.ts+35 −19 modified@@ -35,6 +35,31 @@ export function extname(path: string): string { return match ? match[0] : ''; } +export function join(...segments: string[]): string { + const joined = segments.join('/'); + const absolute = ANY_SLASH_REGEX.test(joined[0]); + return ( + (absolute ? '/' : '') + + (normalizePathSegments(joined.split(ANY_SLASH_REGEX), absolute) || (absolute ? '' : '.')) + ); +} + +function normalizePathSegments(parts: string[], absolute = false): string { + const normalized: string[] = []; + for (const part of parts) { + if (part === '..') { + if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') { + normalized.pop(); + } else if (!absolute) { + normalized.push('..'); + } + } else if (part !== '.' && part !== '') { + normalized.push(part); + } + } + return normalized.join('/'); +} + export function relative(from: string, to: string): string { const fromParts = from.split(ANY_SLASH_REGEX).filter(Boolean); const toParts = to.split(ANY_SLASH_REGEX).filter(Boolean); @@ -60,28 +85,19 @@ export function relative(from: string, to: string): string { } export function resolve(...paths: string[]): string { - const firstPathSegment = paths.shift(); - if (!firstPathSegment) { - return '/'; - } - let resolvedParts = firstPathSegment.split(ANY_SLASH_REGEX); - + let parts: string[] = []; + let isAbsoluteResult = false; for (const path of paths) { if (isAbsolute(path)) { - resolvedParts = path.split(ANY_SLASH_REGEX); + parts = path.split(ANY_SLASH_REGEX); + isAbsoluteResult = true; } else { - const parts = path.split(ANY_SLASH_REGEX); - - while (parts[0] === '.' || parts[0] === '..') { - const part = parts.shift(); - if (part === '..') { - resolvedParts.pop(); - } - } - - resolvedParts.push(...parts); + parts.push(...path.split(ANY_SLASH_REGEX)); } } - - return resolvedParts.join('/'); + const normalized = normalizePathSegments(parts, isAbsoluteResult); + if (!isAbsoluteResult) return normalized || '/'; + // Windows absolute paths (e.g. "C:/path") must not get a leading "/" prepended. + // Unix absolute paths must start with "/". + return /^[A-Za-z]:/.test(normalized) ? normalized : '/' + normalized; }
CHANGELOG.md+8 −0 modified@@ -1,5 +1,13 @@ # rollup changelog +## 3.29.5 + +_2024-09-21_ + +### Bug Fixes + +- Resolve CVE-2024-43788 + ## 3.29.4 _2023-09-28_
src/Bundle.ts+29 −1 modified@@ -22,11 +22,12 @@ import { error, logCannotAssignModuleToChunk, logChunkInvalid, + logFileNameOutsideOutputDirectory, logInvalidOption } from './utils/logs'; import type { OutputBundleWithPlaceholders } from './utils/outputBundle'; import { getOutputBundle, removeUnreferencedAssets } from './utils/outputBundle'; -import { isAbsolute } from './utils/path'; +import { isAbsolute, join } from './utils/path'; import { renderChunks } from './utils/renderChunks'; import { timeEnd, timeStart } from './utils/timers'; import { @@ -96,6 +97,7 @@ export default class Bundle { isWrite ]); this.finaliseAssets(outputBundle); + validateOutputBundleFileNames(outputBundle); timeEnd('generate bundle', 2); timeEnd('GENERATE', 1); @@ -314,3 +316,29 @@ function addModuleToManualChunk( } manualChunkAliasByEntry.set(module, alias); } + +function isFileNameOutsideOutputDirectory(fileName: string): boolean { + // Use join() to normalize ".." segments, then replace backslashes so the + // string checks below work identically on Windows and POSIX. + const normalized = join(fileName).replace(/\\/g, '/'); + return ( + normalized === '..' || + normalized.startsWith('../') || + normalized === '.' || + isAbsolute(normalized) + ); +} + +function validateOutputBundleFileNames(bundle: OutputBundleWithPlaceholders): void { + for (const [bundleKey, entry] of Object.entries(bundle)) { + if (isFileNameOutsideOutputDirectory(bundleKey)) { + return error(logFileNameOutsideOutputDirectory(bundleKey)); + } + if (entry.type !== 'placeholder') { + const { fileName } = entry; + if (fileName !== bundleKey && isFileNameOutsideOutputDirectory(fileName)) { + return error(logFileNameOutsideOutputDirectory(fileName)); + } + } + } +}
src/utils/logs.ts+8 −0 modified@@ -88,6 +88,7 @@ const ADDON_ERROR = 'ADDON_ERROR', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FAIL_AFTER_WARNINGS = 'FAIL_AFTER_WARNINGS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', + FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FIRST_SIDE_EFFECT = 'FIRST_SIDE_EFFECT', ILLEGAL_IDENTIFIER_AS_NAME = 'ILLEGAL_IDENTIFIER_AS_NAME', @@ -357,6 +358,13 @@ export function logFileNameConflict(fileName: string): RollupLog { }; } +export function logFileNameOutsideOutputDirectory(fileName: string): RollupLog { + return { + code: FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY, + message: `The output file name "${fileName}" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.` + }; +} + export function logFileReferenceIdNotFoundForFilename(assetReferenceId: string): RollupLog { return { code: FILE_NOT_FOUND,
src/utils/path.ts+1 −1 modified@@ -15,4 +15,4 @@ export function normalize(path: string): string { return path.replace(BACKSLASH_REGEX, '/'); } -export { basename, dirname, extname, relative, resolve } from 'node:path'; +export { basename, dirname, extname, join, relative, resolve } from 'node:path';
test/browser/samples/error-file-name-absolute-path/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../utils.js'); + +module.exports = defineTest({ + description: 'throws when a plugin adds an absolute file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['/etc/passwd'] = { + type: 'asset', + fileName: '/etc/passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "/etc/passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/browser/samples/error-file-name-path-traversal/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../utils.js'); + +module.exports = defineTest({ + description: 'throws when a plugin adds a path traversal file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['a/../../escaped.js'] = { + type: 'asset', + fileName: 'a/../../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/../../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/browser/samples/error-file-name-windows-absolute-path/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../utils.js'); + +module.exports = defineTest({ + description: 'throws when a plugin adds a Windows-style absolute file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['C:\\etc\\passwd'] = { + type: 'asset', + fileName: 'C:\\etc\\passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "C:\\etc\\passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/browser/samples/renormalizes-external-paths-past-root/_config.js+39 −0 added@@ -0,0 +1,39 @@ +const { join, dirname } = require('node:path').posix; + +module.exports = defineTest({ + description: + 'clamps at root when renormalizing external paths that traverse past the root via resolve', + options: { + input: '/main.js', + external(id) { + return id.endsWith('ext'); + }, + plugins: { + name: 'test-plugin', + resolveId(source, importer) { + if (source.endsWith('ext.js')) { + return false; + } + if (!importer) { + return source; + } + return join(dirname(importer), source); + }, + load(id) { + switch (id) { + case '/main.js': { + // dep.js is at root; the external is imported via '../../escaped-ext.js' + // from dep.js — two levels up from '/' — which should clamp to '/escaped-ext.js' + return `import './dep.js';`; + } + case '/dep.js': { + return `import '../../escaped-ext.js';`; + } + default: { + throw new Error(`Unexpected id ${id}`); + } + } + } + } + } +});
test/browser/samples/renormalizes-external-paths-past-root/_expected/main.js+1 −0 added@@ -0,0 +1 @@ +import './escaped-ext.js';
test/function/samples/error-file-name-absolute-path/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds an absolute file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['/etc/passwd'] = { + type: 'asset', + fileName: '/etc/passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "/etc/passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-absolute-path/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-deep-traversal/_config.js+27 −0 added@@ -0,0 +1,27 @@ +module.exports = defineTest({ + description: + 'throws when a file name resolves outside the output directory after normalizing deep ".." segments', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['a/b/../../../escape.js'] = { + type: 'asset', + fileName: 'a/b/../../../escape.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/b/../../../escape.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-deep-traversal/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-dot/_config.js+14 −0 added@@ -0,0 +1,14 @@ +module.exports = defineTest({ + description: 'throws when entryFileNames is "."', + options: { + output: { + entryFileNames: '.', + format: 'es' + } + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "." is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-dot-dot/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds ".." as a file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['..'] = { + type: 'asset', + fileName: '..', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name ".." is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-dot-dot/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-dot/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-escaped-via-filename-property/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin sets a safe bundle key but an escaping fileName property', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['safe.js'] = { + type: 'asset', + fileName: '../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-escaped-via-filename-property/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-path-traversal/_config.js+14 −0 added@@ -0,0 +1,14 @@ +module.exports = defineTest({ + description: 'throws when entryFileNames contains a mid-path traversal sequence', + options: { + output: { + entryFileNames: 'a/../../pwned.js', + format: 'es' + } + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/../../pwned.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-path-traversal/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-path-traversal-plugin/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds a path traversal file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['../escaped.js'] = { + type: 'asset', + fileName: '../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-path-traversal-plugin/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-windows-absolute-path/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = defineTest({ + description: 'throws when a plugin adds a Windows-style absolute file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['C:\\etc\\passwd'] = { + type: 'asset', + fileName: 'C:\\etc\\passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "C:\\etc\\passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +});
test/function/samples/error-file-name-windows-absolute-path/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-double-dot-prefix-is-valid/_config.js+26 −0 added@@ -0,0 +1,26 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names that start with ".." but are not path traversal sequences', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['..foo.js'] = { + type: 'asset', + fileName: '..foo.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'content' + }; + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-double-dot-prefix-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-leading-dot-slash-is-valid/_config.js+26 −0 added@@ -0,0 +1,26 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names with a leading "./" prefix', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['./asset.txt'] = { + type: 'asset', + fileName: './asset.txt', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'content' + }; + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-leading-dot-slash-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-mid-path-up-then-down-is-valid/_config.js+22 −0 added@@ -0,0 +1,22 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names with ".." that normalize to a path inside the output directory', + options: { + plugins: [ + { + name: 'test', + buildStart() { + this.emitFile({ + type: 'asset', + fileName: 'foo/bar/../baz.js', + source: 'content' + }); + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-mid-path-up-then-down-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-subdirectory-is-valid/_config.js+22 −0 added@@ -0,0 +1,22 @@ +const assert = require('node:assert'); + +module.exports = defineTest({ + description: 'allows file names with subdirectories within the output directory', + options: { + plugins: [ + { + name: 'test', + buildStart() { + this.emitFile({ + type: 'asset', + fileName: 'sub/dir/asset.txt', + source: 'content' + }); + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +});
test/function/samples/file-name-subdirectory-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
d6dee5e99bb8Validate bundle stays within output dir (#6277)
34 files changed · +521 −25
browser/path.ts+39 −23 modified@@ -1,4 +1,4 @@ -const ABSOLUTE_PATH_REGEX = /^(?:\/|(?:[A-Za-z]:)?[\\|/])/; +const ABSOLUTE_PATH_REGEX = /^(?:\/|(?:[A-Za-z]:)?[/\\|])/; const RELATIVE_PATH_REGEX = /^\.?\.\//; const ALL_BACKSLASHES_REGEX = /\\/g; const ANY_SLASH_REGEX = /[/\\]/; @@ -24,17 +24,42 @@ export function dirname(path: string): string { const match = /[/\\][^/\\]*$/.exec(path); if (!match) return '.'; - const dir = path.slice(0, -match[0].length); + const directory = path.slice(0, -match[0].length); - // If `dir` is the empty string, we're at root. - return dir ? dir : '/'; + // If `directory` is the empty string, we're at root. + return directory || '/'; } export function extname(path: string): string { const match = EXTNAME_REGEX.exec(basename(path)!); return match ? match[0] : ''; } +export function join(...segments: string[]): string { + const joined = segments.join('/'); + const absolute = ANY_SLASH_REGEX.test(joined[0]); + return ( + (absolute ? '/' : '') + + (normalizePathSegments(joined.split(ANY_SLASH_REGEX), absolute) || (absolute ? '' : '.')) + ); +} + +function normalizePathSegments(parts: string[], absolute = false): string { + const normalized: string[] = []; + for (const part of parts) { + if (part === '..') { + if (normalized.length > 0 && normalized[normalized.length - 1] !== '..') { + normalized.pop(); + } else if (!absolute) { + normalized.push('..'); + } + } else if (part !== '.' && part !== '') { + normalized.push(part); + } + } + return normalized.join('/'); +} + export function relative(from: string, to: string): string { const fromParts = from.split(ANY_SLASH_REGEX).filter(Boolean); const toParts = to.split(ANY_SLASH_REGEX).filter(Boolean); @@ -60,28 +85,19 @@ export function relative(from: string, to: string): string { } export function resolve(...paths: string[]): string { - const firstPathSegment = paths.shift(); - if (!firstPathSegment) { - return '/'; - } - let resolvedParts = firstPathSegment.split(ANY_SLASH_REGEX); - + let parts: string[] = []; + let isAbsoluteResult = false; for (const path of paths) { if (isAbsolute(path)) { - resolvedParts = path.split(ANY_SLASH_REGEX); + parts = path.split(ANY_SLASH_REGEX); + isAbsoluteResult = true; } else { - const parts = path.split(ANY_SLASH_REGEX); - - while (parts[0] === '.' || parts[0] === '..') { - const part = parts.shift(); - if (part === '..') { - resolvedParts.pop(); - } - } - - resolvedParts.push(...parts); + parts.push(...path.split(ANY_SLASH_REGEX)); } } - - return resolvedParts.join('/'); + const normalized = normalizePathSegments(parts, isAbsoluteResult); + if (!isAbsoluteResult) return normalized || '/'; + // Windows absolute paths (e.g. "C:/path") must not get a leading "/" prepended. + // Unix absolute paths must start with "/". + return /^[A-Za-z]:/.test(normalized) ? normalized : '/' + normalized; }
CHANGELOG.md+24 −0 modified@@ -1,5 +1,29 @@ # rollup changelog +## 2.80.0 + +_2026-02-22_ + +### Features + +- Throw when the generated bundle contains paths that would leave the output directory (#6277) + +### Pull Requests + +- [#6277](https://github.com/rollup/rollup/pull/6277): Validate bundle stays within output dir (@lukastaegert) + +## 2.79.2 + +_2024-09-26_ + +### Bug Fixes + +- Resolve CVE-2024-43788 (#5677) + +### Pull Requests + +- [#5677](https://github.com/rollup/rollup/pull/5677): resolve DOM Clobbering CVE-2024-43788 (backport to v2) (@fabianszabo) + ## 2.79.1 _2022-09-22_
src/Bundle.ts+29 −1 modified@@ -18,6 +18,7 @@ import commondir from './utils/commondir'; import { errCannotAssignModuleToChunk, errChunkInvalid, + errFileNameOutsideOutputDirectory, errInvalidOption, error, warnDeprecation @@ -29,7 +30,7 @@ import { getOutputBundle, OutputBundleWithPlaceholders } from './utils/outputBundle'; -import { basename, isAbsolute } from './utils/path'; +import { basename, isAbsolute, join } from './utils/path'; import { timeEnd, timeStart } from './utils/timers'; export default class Bundle { @@ -80,6 +81,7 @@ export default class Bundle { isWrite ]); this.finaliseAssets(outputBundle); + validateOutputBundleFileNames(outputBundle); timeEnd('GENERATE', 1); return outputBundleBase; @@ -335,3 +337,29 @@ function addModuleToManualChunk( } manualChunkAliasByEntry.set(module, alias); } + +function isFileNameOutsideOutputDirectory(fileName: string): boolean { + // Use join() to normalize ".." segments, then replace backslashes so the + // string checks below work identically on Windows and POSIX. + const normalized = join(fileName).replace(/\\/g, '/'); + return ( + normalized === '..' || + normalized.startsWith('../') || + normalized === '.' || + isAbsolute(normalized) + ); +} + +function validateOutputBundleFileNames(bundle: OutputBundleWithPlaceholders): void { + for (const [bundleKey, entry] of Object.entries(bundle)) { + if (isFileNameOutsideOutputDirectory(bundleKey)) { + return error(errFileNameOutsideOutputDirectory(bundleKey)); + } + if (entry.type !== 'placeholder') { + const { fileName } = entry; + if (fileName !== bundleKey && isFileNameOutsideOutputDirectory(fileName)) { + return error(errFileNameOutsideOutputDirectory(fileName)); + } + } + } +}
src/utils/error.ts+8 −0 modified@@ -52,6 +52,7 @@ export const enum Errors { DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', EXTERNAL_SYNTHETIC_EXPORTS = 'EXTERNAL_SYNTHETIC_EXPORTS', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', + FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY = 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', FILE_NOT_FOUND = 'FILE_NOT_FOUND', INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', @@ -190,6 +191,13 @@ export function errFileNameConflict(fileName: string): RollupLogProps { }; } +export function errFileNameOutsideOutputDirectory(fileName: string): RollupLogProps { + return { + code: Errors.FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY, + message: `The output file name "${fileName}" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.` + }; +} + export function errInputHookInOutputPlugin(pluginName: string, hookName: string): RollupLogProps { return { code: Errors.INPUT_HOOK_IN_OUTPUT_PLUGIN,
src/utils/path.ts+1 −1 modified@@ -15,4 +15,4 @@ export function normalize(path: string): string { return path.replace(BACKSLASH_REGEX, '/'); } -export { basename, dirname, extname, relative, resolve } from 'path'; +export { basename, dirname, extname, join, relative, resolve } from 'path';
test/browser/samples/error-file-name-absolute-path/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../utils.js'); + +module.exports = { + description: 'throws when a plugin adds an absolute file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['/etc/passwd'] = { + type: 'asset', + fileName: '/etc/passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "/etc/passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/browser/samples/error-file-name-path-traversal/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../utils.js'); + +module.exports = { + description: 'throws when a plugin adds a path traversal file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['a/../../escaped.js'] = { + type: 'asset', + fileName: 'a/../../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/../../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/browser/samples/error-file-name-windows-absolute-path/_config.js+29 −0 added@@ -0,0 +1,29 @@ +const { loader } = require('../../../utils.js'); + +module.exports = { + description: 'throws when a plugin adds a Windows-style absolute file name to the output bundle', + options: { + plugins: [ + loader({ main: 'export default 42;' }), + { + name: 'test', + generateBundle(_options, bundle) { + bundle['C:\\etc\\passwd'] = { + type: 'asset', + fileName: 'C:\\etc\\passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "C:\\etc\\passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/browser/samples/renormalizes-external-paths-past-root/_config.js+39 −0 added@@ -0,0 +1,39 @@ +const { join, dirname } = require('node:path').posix; + +module.exports = { + description: + 'clamps at root when renormalizing external paths that traverse past the root via resolve', + options: { + input: '/main.js', + external(id) { + return id.endsWith('ext'); + }, + plugins: { + name: 'test-plugin', + resolveId(source, importer) { + if (source.endsWith('ext.js')) { + return false; + } + if (!importer) { + return source; + } + return join(dirname(importer), source); + }, + load(id) { + switch (id) { + case '/main.js': { + // dep.js is at root; the external is imported via '../../escaped-ext.js' + // from dep.js — two levels up from '/' — which should clamp to '/escaped-ext.js' + return `import './dep.js';`; + } + case '/dep.js': { + return `import '../../escaped-ext.js';`; + } + default: { + throw new Error(`Unexpected id ${id}`); + } + } + } + } + } +};
test/browser/samples/renormalizes-external-paths-past-root/_expected/main.js+1 −0 added@@ -0,0 +1 @@ +import './escaped-ext.js';
test/function/samples/error-file-name-absolute-path/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = { + description: 'throws when a plugin adds an absolute file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['/etc/passwd'] = { + type: 'asset', + fileName: '/etc/passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "/etc/passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-absolute-path/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-deep-traversal/_config.js+27 −0 added@@ -0,0 +1,27 @@ +module.exports = { + description: + 'throws when a file name resolves outside the output directory after normalizing deep ".." segments', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['a/b/../../../escape.js'] = { + type: 'asset', + fileName: 'a/b/../../../escape.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/b/../../../escape.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-deep-traversal/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-dot/_config.js+14 −0 added@@ -0,0 +1,14 @@ +module.exports = { + description: 'throws when entryFileNames is "."', + options: { + output: { + entryFileNames: '.', + format: 'es' + } + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "." is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-dot-dot/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = { + description: 'throws when a plugin adds ".." as a file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['..'] = { + type: 'asset', + fileName: '..', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name ".." is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-dot-dot/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-dot/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-escaped-via-filename-property/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = { + description: 'throws when a plugin sets a safe bundle key but an escaping fileName property', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['safe.js'] = { + type: 'asset', + fileName: '../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-escaped-via-filename-property/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-path-traversal/_config.js+14 −0 added@@ -0,0 +1,14 @@ +module.exports = { + description: 'throws when entryFileNames contains a mid-path traversal sequence', + options: { + output: { + entryFileNames: 'a/../../pwned.js', + format: 'es' + } + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "a/../../pwned.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-path-traversal/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-path-traversal-plugin/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = { + description: 'throws when a plugin adds a path traversal file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['../escaped.js'] = { + type: 'asset', + fileName: '../escaped.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "../escaped.js" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-path-traversal-plugin/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/error-file-name-windows-absolute-path/_config.js+26 −0 added@@ -0,0 +1,26 @@ +module.exports = { + description: 'throws when a plugin adds a Windows-style absolute file name to the output bundle', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['C:\\etc\\passwd'] = { + type: 'asset', + fileName: 'C:\\etc\\passwd', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'injected' + }; + } + } + ] + }, + generateError: { + code: 'FILE_NAME_OUTSIDE_OUTPUT_DIRECTORY', + message: + 'The output file name "C:\\etc\\passwd" is not contained in the output directory. Make sure all file names are relative paths without ".." segments.' + } +};
test/function/samples/error-file-name-windows-absolute-path/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-double-dot-prefix-is-valid/_config.js+26 −0 added@@ -0,0 +1,26 @@ +const assert = require('node:assert'); + +module.exports = { + description: 'allows file names that start with ".." but are not path traversal sequences', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['..foo.js'] = { + type: 'asset', + fileName: '..foo.js', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'content' + }; + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +};
test/function/samples/file-name-double-dot-prefix-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-leading-dot-slash-is-valid/_config.js+26 −0 added@@ -0,0 +1,26 @@ +const assert = require('node:assert'); + +module.exports = { + description: 'allows file names with a leading "./" prefix', + options: { + plugins: [ + { + name: 'test', + generateBundle(_options, bundle) { + bundle['./asset.txt'] = { + type: 'asset', + fileName: './asset.txt', + name: undefined, + needsCodeReference: false, + names: [], + originalFileNames: [], + source: 'content' + }; + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +};
test/function/samples/file-name-leading-dot-slash-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-mid-path-up-then-down-is-valid/_config.js+22 −0 added@@ -0,0 +1,22 @@ +const assert = require('node:assert'); + +module.exports = { + description: 'allows file names with ".." that normalize to a path inside the output directory', + options: { + plugins: [ + { + name: 'test', + buildStart() { + this.emitFile({ + type: 'asset', + fileName: 'foo/bar/../baz.js', + source: 'content' + }); + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +};
test/function/samples/file-name-mid-path-up-then-down-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
test/function/samples/file-name-subdirectory-is-valid/_config.js+22 −0 added@@ -0,0 +1,22 @@ +const assert = require('node:assert'); + +module.exports = { + description: 'allows file names with subdirectories within the output directory', + options: { + plugins: [ + { + name: 'test', + buildStart() { + this.emitFile({ + type: 'asset', + fileName: 'sub/dir/asset.txt', + source: 'content' + }); + } + } + ] + }, + code(result) { + assert.ok(result !== null && result !== undefined, 'bundle was generated successfully'); + } +};
test/function/samples/file-name-subdirectory-is-valid/main.js+1 −0 added@@ -0,0 +1 @@ +export default 42;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-mw96-cpmx-2vgcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27606ghsaADVISORY
- github.com/rollup/rollup/commit/c60770d7aaf750e512c1b2774989ea4596e660b2ghsax_refsource_MISCWEB
- github.com/rollup/rollup/commit/c8cf1f9c48c516285758c1e11f08a54f304fd44eghsax_refsource_MISCWEB
- github.com/rollup/rollup/commit/d6dee5e99bb82aac0bee1df4ab9efbde455452c3ghsax_refsource_MISCWEB
- github.com/rollup/rollup/releases/tag/v2.80.0ghsax_refsource_MISCWEB
- github.com/rollup/rollup/releases/tag/v3.30.0ghsax_refsource_MISCWEB
- github.com/rollup/rollup/releases/tag/v4.59.0ghsax_refsource_MISCWEB
- github.com/rollup/rollup/security/advisories/GHSA-mw96-cpmx-2vgcghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.