@tschk/moonshine-deploy-node 0.3.2 → 0.3.3

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.
Files changed (2) hide show
  1. package/package.json +6 -6
  2. package/src/index.ts +20 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-deploy-node",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -15,11 +15,11 @@
15
15
  "test": "bun test"
16
16
  },
17
17
  "dependencies": {
18
- "@tschk/moonshine-adapter-conformance": "^0.3.2",
19
- "@tschk/moonshine-compiler": "^0.3.2",
20
- "@tschk/moonshine-framework": "^0.3.2",
21
- "@tschk/moonshine-react": "^0.3.2",
22
- "@tschk/moonshine-server": "^0.3.2"
18
+ "@tschk/moonshine-adapter-conformance": "^0.3.3",
19
+ "@tschk/moonshine-compiler": "^0.3.3",
20
+ "@tschk/moonshine-framework": "^0.3.3",
21
+ "@tschk/moonshine-react": "^0.3.3",
22
+ "@tschk/moonshine-server": "^0.3.3"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/bun": "1.3.14",
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  type DeploymentAdapter,
12
12
  type MoonshineManifest,
13
13
  } from "@tschk/moonshine-framework";
14
- import { resolveStaticPath } from "@tschk/moonshine-server";
14
+ import { isContained, resolveStaticPath } from "@tschk/moonshine-server";
15
15
  import {
16
16
  type Harness,
17
17
  type HarnessFactory,
@@ -71,12 +71,18 @@ async function tryServeNodeStatic(
71
71
  return null;
72
72
  }
73
73
  if (!stats.isFile()) return null;
74
+ try {
75
+ if (!isContained(staticDir, filePath)) return null;
76
+ } catch {
77
+ return null;
78
+ }
74
79
  const type = MIME[extOf(filePath)] ?? "application/octet-stream";
75
80
  const stream = createReadStream(filePath);
76
81
  return new Response(Readable.toWeb(stream) as unknown as ReadableStream, {
77
82
  headers: {
78
83
  "content-type": type,
79
84
  "content-length": String(stats.size),
85
+ "x-content-type-options": "nosniff",
80
86
  },
81
87
  });
82
88
  }
@@ -104,6 +110,18 @@ async function sendNodeResponse(
104
110
  stream.pipe(res);
105
111
  }
106
112
 
113
+ const HOST_PATTERN = /^(?:[a-zA-Z0-9._-]+|\[[0-9a-fA-F:.]+\])(?::\d{1,5})?$/;
114
+
115
+ /**
116
+ * Node hands the Host header through unvalidated, so a client-supplied value
117
+ * can smuggle userinfo, a path, or a query into the base URL and change the
118
+ * origin every downstream URL comparison is made against.
119
+ */
120
+ function safeHost(host: string | undefined): string {
121
+ if (!host) return "localhost";
122
+ return HOST_PATTERN.test(host) ? host : "localhost";
123
+ }
124
+
107
125
  export type NodeHandlerOptions = {
108
126
  fetch: (request: Request) => Promise<Response>;
109
127
  staticDir?: string;
@@ -115,7 +133,7 @@ export function createNodeHandler(
115
133
  return async (req, res) => {
116
134
  const method = req.method ?? "GET";
117
135
  const reqUrl = req.url ?? "/";
118
- const host = req.headers.host ?? "localhost";
136
+ const host = safeHost(req.headers.host);
119
137
  const requestUrl = new URL(reqUrl, `http://${host}`);
120
138
  const pathname = requestUrl.pathname;
121
139