@cjhyy/code-shell-server 0.9.0 → 0.9.1

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.
@@ -27,7 +27,23 @@ export declare function mobileAssetPath(reqUrl: string): string;
27
27
  export declare function mobileEntryRedirect(reqUrl: string): string | null;
28
28
  /** Resolve an asset sub-path to an absolute file inside `root`, or null if it
29
29
  * would escape the root (traversal) or does not resolve to a regular file. An
30
- * empty sub-path resolves to index.html (SPA entry). */
30
+ * empty sub-path resolves to index.html (SPA entry).
31
+ *
32
+ * Containment is checked on the REAL paths (both sides `realpathSync`d), not on
33
+ * the lexical path: `statSync` follows symlinks, so a link sitting inside the
34
+ * root but pointing outside it — e.g. a `node_modules` link left by a packaging
35
+ * step — used to pass a string-prefix check and serve the target's contents.
36
+ * This covers symlinks (including chains and symlinked directories) but NOT
37
+ * hardlinks: a hardlink IS a real name inside the root, so no path-level check
38
+ * can see through it. Guarding that would need inode/device policy, and anyone
39
+ * able to hardlink into the served root can already write files there.
40
+ *
41
+ * The returned path is the REQUESTED one, not the realpath: callers stream it
42
+ * directly and the resolved-name contract is relied on elsewhere (on macOS the
43
+ * realpath of a /var root is /private/var, which would change every result).
44
+ * That leaves the same synchronous stat-then-read window this module always
45
+ * had; closing it properly needs an open-then-fstat rework of both callers,
46
+ * which is out of scope for this fix. */
31
47
  export declare function resolveSafe(root: string, subPath: string): string | null;
32
48
  export interface MobileStaticOptions {
33
49
  /** Absolute path to the built mobile app (out/mobile). */
@@ -1,5 +1,5 @@
1
- import { createReadStream, existsSync, statSync } from "node:fs";
2
- import { join, normalize, resolve, sep, extname } from "node:path";
1
+ import { createReadStream, existsSync, realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, normalize, relative, resolve, sep, extname } from "node:path";
3
3
  import { request as httpRequest } from "node:http";
4
4
  import { contentTypeFor } from "../static-files.js";
5
5
  /**
@@ -41,9 +41,32 @@ export function mobileEntryRedirect(reqUrl) {
41
41
  return "/mobile/" + suffix;
42
42
  return null;
43
43
  }
44
+ /** True when `target` is `root` itself or sits underneath it. Uses `relative`
45
+ * rather than a `startsWith` prefix compare so a sibling directory sharing the
46
+ * root's name prefix (`/srv/app-old` vs `/srv/app`) can't slip through. */
47
+ function isContained(root, target) {
48
+ const rel = relative(root, target);
49
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
50
+ }
44
51
  /** Resolve an asset sub-path to an absolute file inside `root`, or null if it
45
52
  * would escape the root (traversal) or does not resolve to a regular file. An
46
- * empty sub-path resolves to index.html (SPA entry). */
53
+ * empty sub-path resolves to index.html (SPA entry).
54
+ *
55
+ * Containment is checked on the REAL paths (both sides `realpathSync`d), not on
56
+ * the lexical path: `statSync` follows symlinks, so a link sitting inside the
57
+ * root but pointing outside it — e.g. a `node_modules` link left by a packaging
58
+ * step — used to pass a string-prefix check and serve the target's contents.
59
+ * This covers symlinks (including chains and symlinked directories) but NOT
60
+ * hardlinks: a hardlink IS a real name inside the root, so no path-level check
61
+ * can see through it. Guarding that would need inode/device policy, and anyone
62
+ * able to hardlink into the served root can already write files there.
63
+ *
64
+ * The returned path is the REQUESTED one, not the realpath: callers stream it
65
+ * directly and the resolved-name contract is relied on elsewhere (on macOS the
66
+ * realpath of a /var root is /private/var, which would change every result).
67
+ * That leaves the same synchronous stat-then-read window this module always
68
+ * had; closing it properly needs an open-then-fstat rework of both callers,
69
+ * which is out of scope for this fix. */
47
70
  export function resolveSafe(root, subPath) {
48
71
  const rootResolved = resolve(root);
49
72
  const rel = normalize(subPath || "index.html");
@@ -52,10 +75,19 @@ export function resolveSafe(root, subPath) {
52
75
  return null;
53
76
  }
54
77
  const full = resolve(join(rootResolved, rel));
55
- if (full !== rootResolved && !full.startsWith(rootResolved + sep))
78
+ if (!isContained(rootResolved, full))
56
79
  return null;
57
80
  if (!existsSync(full) || !statSync(full).isFile())
58
81
  return null;
82
+ // realpathSync throws on a dangling link or a race-deleted file; a missing
83
+ // target is a 404 through the normal contract, never a 500.
84
+ try {
85
+ if (!isContained(realpathSync(rootResolved), realpathSync(full)))
86
+ return null;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
59
91
  return full;
60
92
  }
61
93
  /** True when the request is a browser navigation (accepts text/html), used to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-server",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "CodeShell server transport layer — mobile remote HTTP/WS host, pairing, passcode gate, tunnel, rooms, uploads, and session/attachment services. Electron-free; precursor of the auth gateway.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,9 +39,9 @@
39
39
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
40
40
  },
41
41
  "dependencies": {
42
- "@cjhyy/code-shell-capability-coding": "0.9.0",
43
- "@cjhyy/code-shell-core": "0.9.0",
44
- "@cjhyy/code-shell-web": "0.9.0",
42
+ "@cjhyy/code-shell-capability-coding": "0.9.1",
43
+ "@cjhyy/code-shell-core": "0.9.1",
44
+ "@cjhyy/code-shell-web": "0.9.1",
45
45
  "@types/ws": "^8.18.1",
46
46
  "ws": "^8.21.0"
47
47
  },