@cjhyy/code-shell-server 0.9.0 → 0.9.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.
|
@@ -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 (
|
|
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.
|
|
3
|
+
"version": "0.9.2",
|
|
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.
|
|
43
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
44
|
-
"@cjhyy/code-shell-web": "0.9.
|
|
42
|
+
"@cjhyy/code-shell-capability-coding": "0.9.2",
|
|
43
|
+
"@cjhyy/code-shell-core": "0.9.2",
|
|
44
|
+
"@cjhyy/code-shell-web": "0.9.2",
|
|
45
45
|
"@types/ws": "^8.18.1",
|
|
46
46
|
"ws": "^8.21.0"
|
|
47
47
|
},
|