@lwrjs/shared-utils 0.23.19 → 0.23.21

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.
@@ -37,8 +37,10 @@ __export(exports, {
37
37
  VERSION_NOT_PROVIDED: () => VERSION_NOT_PROVIDED,
38
38
  VERSION_PREFIX: () => VERSION_PREFIX,
39
39
  VERSION_SIGIL: () => VERSION_SIGIL,
40
+ containsPathTraversal: () => containsPathTraversal,
40
41
  explodeSpecifier: () => explodeSpecifier,
41
42
  explodeSpecifiers: () => explodeSpecifiers,
43
+ fullyDecodeSpecifier: () => fullyDecodeSpecifier,
42
44
  getCacheKeyFromJson: () => import_fast_json_stable_stringify.default,
43
45
  getMappingUriPrefix: () => getMappingUriPrefix,
44
46
  getModuleUriPrefix: () => getModuleUriPrefix,
@@ -85,7 +87,30 @@ function normalizeVersionFromUri(version) {
85
87
  function prettyModuleUriSuffix(specifier) {
86
88
  return `${specifier.replace(/[\/\.\?\#]/g, "_")}`;
87
89
  }
90
+ var MAX_DECODE_ITERATIONS = 5;
91
+ function fullyDecodeSpecifier(rawSpecifier) {
92
+ let decoded = rawSpecifier;
93
+ for (let i = 0; i < MAX_DECODE_ITERATIONS; i++) {
94
+ let next;
95
+ try {
96
+ next = decodeURIComponent(decoded);
97
+ } catch {
98
+ return decoded;
99
+ }
100
+ if (next === decoded) {
101
+ return decoded;
102
+ }
103
+ decoded = next;
104
+ }
105
+ throw new Error(`Specifier "${rawSpecifier}" could not be fully decoded within ${MAX_DECODE_ITERATIONS} passes`);
106
+ }
107
+ function containsPathTraversal(decodedSpecifier) {
108
+ return decodedSpecifier.split(/[\\/#?]/).some((segment) => segment === "..");
109
+ }
88
110
  function explodeSpecifier(rawSpecifier) {
111
+ if (containsPathTraversal(fullyDecodeSpecifier(rawSpecifier))) {
112
+ throw new Error(`Invalid module specifier "${rawSpecifier}": path traversal is not allowed`);
113
+ }
89
114
  const decodedSpecifier = decodeURIComponent(rawSpecifier);
90
115
  const versionMatch = decodedSpecifier.match(/(.+)\/v\/([a-zA-Z0-9-_.]+)$/);
91
116
  const version = versionMatch ? versionMatch[2] : void 0;
@@ -35,6 +35,23 @@ export declare function normalizeVersionFromUri(version: string): string;
35
35
  * examples/app/scoped/css?scoped=true => examples_app_scoped_css_scoped=true
36
36
  */
37
37
  export declare function prettyModuleUriSuffix(specifier: string): string;
38
+ /**
39
+ * Repeatedly percent-decodes a string until it stabilizes (or the iteration bound is hit).
40
+ * A single decode pass can be defeated by nesting encodings (e.g. '%252e%252e%252f'
41
+ * decodes once to '%2e%2e%2f', which looks harmless until it's decoded again). Fully
42
+ * resolving the encoding first means a later traversal check can't be smuggled past.
43
+ */
44
+ export declare function fullyDecodeSpecifier(rawSpecifier: string): string;
45
+ /**
46
+ * True if any '/'-, '\'-, '#'-, or '?'-delimited segment of an already-decoded specifier
47
+ * is exactly '..' (a directory traversal segment). Segment based so legitimate names
48
+ * containing dots (e.g. 'my.label') are never mistaken for traversal. '#' and '?' are
49
+ * included because callers (e.g. lwc-module-provider) split the fragment after '#' off
50
+ * of a specifier and path.join it against a resolved directory - a '..' glued directly
51
+ * after '#' (e.g. 'c/app#../secret') would otherwise never appear as its own '/'-delimited
52
+ * segment and slip past this check (W-24189528).
53
+ */
54
+ export declare function containsPathTraversal(decodedSpecifier: string): boolean;
38
55
  /**
39
56
  * Create a partial Module Identifier from a specifier
40
57
  * The Module Identifier is partial because the specifier may not contain a version
@@ -42,7 +42,50 @@ export function prettyModuleUriSuffix(specifier) {
42
42
  // eslint-disable-next-line no-useless-escape
43
43
  return `${specifier.replace(/[\/\.\?\#]/g, '_')}`;
44
44
  }
45
+ const MAX_DECODE_ITERATIONS = 5;
46
+ /**
47
+ * Repeatedly percent-decodes a string until it stabilizes (or the iteration bound is hit).
48
+ * A single decode pass can be defeated by nesting encodings (e.g. '%252e%252e%252f'
49
+ * decodes once to '%2e%2e%2f', which looks harmless until it's decoded again). Fully
50
+ * resolving the encoding first means a later traversal check can't be smuggled past.
51
+ */
52
+ export function fullyDecodeSpecifier(rawSpecifier) {
53
+ let decoded = rawSpecifier;
54
+ for (let i = 0; i < MAX_DECODE_ITERATIONS; i++) {
55
+ let next;
56
+ try {
57
+ next = decodeURIComponent(decoded);
58
+ }
59
+ catch {
60
+ return decoded; // malformed escape sequence, nothing further to decode
61
+ }
62
+ if (next === decoded) {
63
+ return decoded; // fully stable within budget
64
+ }
65
+ decoded = next;
66
+ }
67
+ // Every pass up to the budget still changed the string, so the encoding
68
+ // depth is unknown and possibly unbounded. Fail closed instead of checking
69
+ // a value that might still have traversal segments hidden behind more
70
+ // encoding than we were willing to unwrap.
71
+ throw new Error(`Specifier "${rawSpecifier}" could not be fully decoded within ${MAX_DECODE_ITERATIONS} passes`);
72
+ }
73
+ /**
74
+ * True if any '/'-, '\'-, '#'-, or '?'-delimited segment of an already-decoded specifier
75
+ * is exactly '..' (a directory traversal segment). Segment based so legitimate names
76
+ * containing dots (e.g. 'my.label') are never mistaken for traversal. '#' and '?' are
77
+ * included because callers (e.g. lwc-module-provider) split the fragment after '#' off
78
+ * of a specifier and path.join it against a resolved directory - a '..' glued directly
79
+ * after '#' (e.g. 'c/app#../secret') would otherwise never appear as its own '/'-delimited
80
+ * segment and slip past this check (W-24189528).
81
+ */
82
+ export function containsPathTraversal(decodedSpecifier) {
83
+ return decodedSpecifier.split(/[\\/#?]/).some((segment) => segment === '..');
84
+ }
45
85
  export function explodeSpecifier(rawSpecifier) {
86
+ if (containsPathTraversal(fullyDecodeSpecifier(rawSpecifier))) {
87
+ throw new Error(`Invalid module specifier "${rawSpecifier}": path traversal is not allowed`);
88
+ }
46
89
  const decodedSpecifier = decodeURIComponent(rawSpecifier);
47
90
  // Split up the version and bare specifier
48
91
  const versionMatch = decodedSpecifier.match(/(.+)\/v\/([a-zA-Z0-9-_.]+)$/);
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.23.19",
7
+ "version": "0.23.21",
8
8
  "homepage": "https://developer.salesforce.com/docs/platform/lwr/overview",
9
9
  "repository": {
10
10
  "type": "git",
@@ -37,7 +37,7 @@
37
37
  "build/**/*.d.ts"
38
38
  ],
39
39
  "dependencies": {
40
- "@lwrjs/diagnostics": "0.23.19",
40
+ "@lwrjs/diagnostics": "0.23.21",
41
41
  "es-module-lexer": "^1.5.4",
42
42
  "fast-json-stable-stringify": "^2.1.0",
43
43
  "magic-string": "^0.30.9",
@@ -50,7 +50,7 @@
50
50
  "slugify": "^1.4.5"
51
51
  },
52
52
  "devDependencies": {
53
- "@lwrjs/types": "0.23.19",
53
+ "@lwrjs/types": "0.23.21",
54
54
  "@types/mime-types": "2.1.4",
55
55
  "@types/path-to-regexp": "^1.7.0",
56
56
  "memfs": "^4.13.0"
@@ -58,5 +58,5 @@
58
58
  "engines": {
59
59
  "node": ">=22.0.0"
60
60
  },
61
- "gitHead": "cb801a885519bb0446bbe5b7d36021c443f72c5b"
61
+ "gitHead": "c20f56af604d81026133ef9e9a575275056e7a91"
62
62
  }