@pnpm/patching.apply-patch 1100.0.0 → 1100.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > Apply a patch to a directory
4
4
 
5
5
  <!--@shields('npm')-->
6
- [![npm version](https://img.shields.io/npm/v/@pnpm/patching.apply-patch.svg)](https://www.npmjs.com/package/@pnpm/patching.apply-patch)
6
+ [![npm version](https://img.shields.io/npm/v/@pnpm/patching.apply-patch.svg)](https://npmx.dev/package/@pnpm/patching.apply-patch)
7
7
  <!--/@-->
8
8
 
9
9
  ## Installation
package/lib/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ import { PnpmError } from '@pnpm/error';
1
2
  export interface ApplyPatchToDirOpts {
2
3
  patchedDir: string;
3
4
  patchFilePath: string;
4
5
  }
5
6
  export declare function applyPatchToDir(opts: ApplyPatchToDirOpts): boolean;
7
+ export declare class PatchPathEscapesError extends PnpmError {
8
+ constructor(opts: ApplyPatchToDirOpts, badPath: string);
9
+ }
package/lib/index.js CHANGED
@@ -1,8 +1,13 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import util from 'node:util';
1
4
  import { PnpmError } from '@pnpm/error';
2
5
  import { applyPatch } from '@pnpm/patch-package/dist/applyPatches.js';
6
+ import { parsePatchFile } from '@pnpm/patch-package/dist/patch/parse.js';
3
7
  export function applyPatchToDir(opts) {
4
8
  // Ideally, we would just run "patch" or "git apply".
5
9
  // However, "patch" is not available on Windows and "git apply" is hard to execute on a subdirectory of an existing repository
10
+ assertPatchPathsStayInside(opts);
6
11
  const cwd = process.cwd();
7
12
  process.chdir(opts.patchedDir);
8
13
  let success = false;
@@ -11,11 +16,12 @@ export function applyPatchToDir(opts) {
11
16
  patchFilePath: opts.patchFilePath,
12
17
  });
13
18
  }
14
- catch (err) { // eslint-disable-line
15
- if (err.code === 'ENOENT') {
19
+ catch (err) {
20
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
16
21
  throw new PnpmError('PATCH_NOT_FOUND', `Patch file not found: ${opts.patchFilePath}`);
17
22
  }
18
- throw new PnpmError('INVALID_PATCH', `Applying patch "${opts.patchFilePath}" failed: ${err.message}`);
23
+ const message = util.types.isNativeError(err) ? err.message : String(err);
24
+ throw new PnpmError('INVALID_PATCH', `Applying patch "${opts.patchFilePath}" failed: ${message}`);
19
25
  }
20
26
  finally {
21
27
  process.chdir(cwd);
@@ -25,4 +31,53 @@ export function applyPatchToDir(opts) {
25
31
  }
26
32
  return success;
27
33
  }
34
+ // A patch file is attacker-controlled data: `diff --git a/../../X b/../../X` headers
35
+ // would otherwise let the applier traverse out of the package directory and write,
36
+ // delete, or rename files anywhere the install user can.
37
+ function assertPatchPathsStayInside(opts) {
38
+ let patchContent;
39
+ try {
40
+ patchContent = fs.readFileSync(opts.patchFilePath, 'utf8');
41
+ }
42
+ catch (err) {
43
+ if (util.types.isNativeError(err) && 'code' in err && err.code === 'ENOENT') {
44
+ throw new PnpmError('PATCH_NOT_FOUND', `Patch file not found: ${opts.patchFilePath}`);
45
+ }
46
+ throw err;
47
+ }
48
+ let effects;
49
+ try {
50
+ effects = parsePatchFile(patchContent);
51
+ }
52
+ catch {
53
+ // Defer parse-error reporting to applyPatch so its existing
54
+ // ERR_PNPM_INVALID_PATCH path produces the message and exit behavior.
55
+ return;
56
+ }
57
+ const root = path.resolve(opts.patchedDir);
58
+ const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
59
+ for (const effect of effects) {
60
+ const candidates = effect.type === 'rename'
61
+ ? [effect.fromPath, effect.toPath]
62
+ : [effect.path];
63
+ for (const candidate of candidates) {
64
+ if (!candidate)
65
+ continue;
66
+ if (path.isAbsolute(candidate) ||
67
+ candidate.split(/[/\\]/).includes('..')) {
68
+ throw new PatchPathEscapesError(opts, candidate);
69
+ }
70
+ const resolved = path.resolve(root, candidate);
71
+ if (resolved !== root && !resolved.startsWith(rootWithSep)) {
72
+ throw new PatchPathEscapesError(opts, candidate);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ export class PatchPathEscapesError extends PnpmError {
78
+ constructor(opts, badPath) {
79
+ super('PATCH_FAILED', `Could not apply patch ${opts.patchFilePath} to ${opts.patchedDir}: ` +
80
+ `patch path escapes target dir: ${badPath}`);
81
+ }
82
+ }
28
83
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnpm/patching.apply-patch",
3
- "version": "1100.0.0",
3
+ "version": "1100.0.2",
4
4
  "description": "Apply a patch to a directory",
5
5
  "keywords": [
6
6
  "pnpm",
@@ -9,7 +9,10 @@
9
9
  ],
10
10
  "license": "MIT",
11
11
  "funding": "https://opencollective.com/pnpm",
12
- "repository": "https://github.com/pnpm/pnpm/tree/main/patching/apply-patch",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/pnpm/pnpm/tree/main/patching/apply-patch"
15
+ },
13
16
  "homepage": "https://github.com/pnpm/pnpm/tree/main/patching/apply-patch#readme",
14
17
  "bugs": {
15
18
  "url": "https://github.com/pnpm/pnpm/issues"
@@ -29,14 +32,14 @@
29
32
  "@pnpm/error": "1100.0.0"
30
33
  },
31
34
  "peerDependencies": {
32
- "@pnpm/logger": ">=1001.0.0 <1002.0.0"
35
+ "@pnpm/logger": "^1100.0.0"
33
36
  },
34
37
  "devDependencies": {
35
- "@jest/globals": "30.3.0",
38
+ "@jest/globals": "30.4.1",
39
+ "@pnpm/prepare": "1100.0.16",
36
40
  "@pnpm/logger": "1100.0.0",
37
- "@pnpm/patching.apply-patch": "1100.0.0",
38
- "@pnpm/prepare": "1100.0.0",
39
- "@pnpm/test-fixtures": "1100.0.0"
41
+ "@pnpm/test-fixtures": "1100.0.0",
42
+ "@pnpm/patching.apply-patch": "1100.0.2"
40
43
  },
41
44
  "engines": {
42
45
  "node": ">=22.13"