@remix-run/static-middleware 0.4.10 → 0.4.11

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.
@@ -1 +1 @@
1
- {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/lib/static.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAInG;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC;IACnF;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAElC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAA;IAE7C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IAC1B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA8EtF"}
1
+ {"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/lib/static.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEzD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AASnG;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC;IACnF;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAElC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAA;IAE7C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IAC1B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAwGtF"}
@@ -1,6 +1,7 @@
1
1
  import * as path from 'node:path';
2
2
  import * as fsp from 'node:fs/promises';
3
3
  import { openLazyFile } from '@remix-run/fs';
4
+ import { detectMimeType } from '@remix-run/mime';
4
5
  import { createFileResponse as sendFile } from '@remix-run/response/file';
5
6
  import { generateDirectoryListing } from "./directory-listing.js";
6
7
  /**
@@ -36,21 +37,42 @@ export function staticFiles(root, options = {}) {
36
37
  if (filter && !filter(relativePath)) {
37
38
  return next();
38
39
  }
40
+ let rootRealPath;
41
+ try {
42
+ rootRealPath = await fsp.realpath(root);
43
+ }
44
+ catch {
45
+ return next();
46
+ }
39
47
  let targetPath = path.join(root, relativePath);
40
- let filePath;
48
+ let containedTargetPath = await resolveContainedPath(rootRealPath, targetPath);
49
+ if (containedTargetPath == null) {
50
+ return next();
51
+ }
52
+ let file;
41
53
  try {
42
- let stats = await fsp.stat(targetPath);
54
+ let stats = await fsp.stat(containedTargetPath);
43
55
  if (stats.isFile()) {
44
- filePath = targetPath;
56
+ file = {
57
+ realPath: containedTargetPath,
58
+ requestedPath: targetPath,
59
+ };
45
60
  }
46
61
  else if (stats.isDirectory()) {
47
62
  // Try each index file in turn
48
63
  for (let indexFile of index) {
49
64
  let indexPath = path.join(targetPath, indexFile);
65
+ let containedIndexPath = await resolveContainedPath(rootRealPath, indexPath);
66
+ if (containedIndexPath == null) {
67
+ continue;
68
+ }
50
69
  try {
51
- let indexStats = await fsp.stat(indexPath);
70
+ let indexStats = await fsp.stat(containedIndexPath);
52
71
  if (indexStats.isFile()) {
53
- filePath = indexPath;
72
+ file = {
73
+ realPath: containedIndexPath,
74
+ requestedPath: indexPath,
75
+ };
54
76
  break;
55
77
  }
56
78
  }
@@ -59,17 +81,20 @@ export function staticFiles(root, options = {}) {
59
81
  }
60
82
  }
61
83
  // If no index file found and listFiles is enabled, show directory listing
62
- if (!filePath && listFiles) {
63
- return generateDirectoryListing(targetPath, context.url.pathname);
84
+ if (!file && listFiles) {
85
+ return generateDirectoryListing(containedTargetPath, context.url.pathname);
64
86
  }
65
87
  }
66
88
  }
67
89
  catch {
68
90
  // Path doesn't exist or isn't accessible, fall through
69
91
  }
70
- if (filePath) {
71
- let fileName = path.relative(root, filePath);
72
- let lazyFile = openLazyFile(filePath, { name: fileName });
92
+ if (file) {
93
+ let fileName = path.relative(root, file.requestedPath);
94
+ let lazyFile = openLazyFile(file.realPath, {
95
+ name: fileName,
96
+ type: detectMimeType(file.requestedPath) ?? '',
97
+ });
73
98
  let finalFileOptions = { ...fileOptions };
74
99
  // If acceptRanges is a function, evaluate it with the lazyFile
75
100
  // Otherwise, pass it directly to sendFile
@@ -84,3 +109,16 @@ export function staticFiles(root, options = {}) {
84
109
  return next();
85
110
  };
86
111
  }
112
+ async function resolveContainedPath(rootRealPath, targetPath) {
113
+ try {
114
+ let realPath = await fsp.realpath(targetPath);
115
+ return isContainedPath(rootRealPath, realPath) ? realPath : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ function isContainedPath(rootPath, targetPath) {
122
+ let relativePath = path.relative(rootPath, targetPath);
123
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/static-middleware",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Middleware for serving static files from the filesystem",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -29,19 +29,19 @@
29
29
  "@types/node": "^24.6.0",
30
30
  "@typescript/native-preview": "7.0.0-dev.20251125.1",
31
31
  "@remix-run/assert": "0.2.1",
32
- "@remix-run/fetch-router": "0.19.1",
33
- "@remix-run/form-data-middleware": "0.3.1",
34
- "@remix-run/method-override-middleware": "0.1.9",
32
+ "@remix-run/fetch-router": "0.19.2",
33
+ "@remix-run/form-data-middleware": "0.3.2",
35
34
  "@remix-run/mime": "0.4.1",
36
- "@remix-run/response": "0.3.5",
37
- "@remix-run/test": "0.4.1"
35
+ "@remix-run/response": "0.3.6",
36
+ "@remix-run/test": "0.4.2",
37
+ "@remix-run/method-override-middleware": "0.1.10"
38
38
  },
39
39
  "dependencies": {
40
- "@remix-run/fs": "^0.4.4",
41
- "@remix-run/html-template": "^0.3.1",
42
- "@remix-run/fetch-router": "^0.19.1",
40
+ "@remix-run/fetch-router": "^0.19.2",
43
41
  "@remix-run/mime": "^0.4.1",
44
- "@remix-run/response": "^0.3.5"
42
+ "@remix-run/fs": "^0.4.5",
43
+ "@remix-run/response": "^0.3.6",
44
+ "@remix-run/html-template": "^0.3.1"
45
45
  },
46
46
  "keywords": [
47
47
  "fetch",
package/src/lib/static.ts CHANGED
@@ -2,10 +2,16 @@ import * as path from 'node:path'
2
2
  import * as fsp from 'node:fs/promises'
3
3
  import { openLazyFile } from '@remix-run/fs'
4
4
  import type { Middleware } from '@remix-run/fetch-router'
5
+ import { detectMimeType } from '@remix-run/mime'
5
6
  import { createFileResponse as sendFile, type FileResponseOptions } from '@remix-run/response/file'
6
7
 
7
8
  import { generateDirectoryListing } from './directory-listing.ts'
8
9
 
10
+ interface StaticFileMatch {
11
+ realPath: string
12
+ requestedPath: string
13
+ }
14
+
9
15
  /**
10
16
  * Function that determines if HTTP Range requests should be supported for a given file.
11
17
  *
@@ -107,22 +113,45 @@ export function staticFiles(root: string, options: StaticFilesOptions = {}): Mid
107
113
  return next()
108
114
  }
109
115
 
116
+ let rootRealPath: string
117
+ try {
118
+ rootRealPath = await fsp.realpath(root)
119
+ } catch {
120
+ return next()
121
+ }
122
+
110
123
  let targetPath = path.join(root, relativePath)
111
- let filePath: string | undefined
124
+ let containedTargetPath = await resolveContainedPath(rootRealPath, targetPath)
125
+ if (containedTargetPath == null) {
126
+ return next()
127
+ }
128
+
129
+ let file: StaticFileMatch | undefined
112
130
 
113
131
  try {
114
- let stats = await fsp.stat(targetPath)
132
+ let stats = await fsp.stat(containedTargetPath)
115
133
 
116
134
  if (stats.isFile()) {
117
- filePath = targetPath
135
+ file = {
136
+ realPath: containedTargetPath,
137
+ requestedPath: targetPath,
138
+ }
118
139
  } else if (stats.isDirectory()) {
119
140
  // Try each index file in turn
120
141
  for (let indexFile of index) {
121
142
  let indexPath = path.join(targetPath, indexFile)
143
+ let containedIndexPath = await resolveContainedPath(rootRealPath, indexPath)
144
+ if (containedIndexPath == null) {
145
+ continue
146
+ }
147
+
122
148
  try {
123
- let indexStats = await fsp.stat(indexPath)
149
+ let indexStats = await fsp.stat(containedIndexPath)
124
150
  if (indexStats.isFile()) {
125
- filePath = indexPath
151
+ file = {
152
+ realPath: containedIndexPath,
153
+ requestedPath: indexPath,
154
+ }
126
155
  break
127
156
  }
128
157
  } catch {
@@ -131,17 +160,20 @@ export function staticFiles(root: string, options: StaticFilesOptions = {}): Mid
131
160
  }
132
161
 
133
162
  // If no index file found and listFiles is enabled, show directory listing
134
- if (!filePath && listFiles) {
135
- return generateDirectoryListing(targetPath, context.url.pathname)
163
+ if (!file && listFiles) {
164
+ return generateDirectoryListing(containedTargetPath, context.url.pathname)
136
165
  }
137
166
  }
138
167
  } catch {
139
168
  // Path doesn't exist or isn't accessible, fall through
140
169
  }
141
170
 
142
- if (filePath) {
143
- let fileName = path.relative(root, filePath)
144
- let lazyFile = openLazyFile(filePath, { name: fileName })
171
+ if (file) {
172
+ let fileName = path.relative(root, file.requestedPath)
173
+ let lazyFile = openLazyFile(file.realPath, {
174
+ name: fileName,
175
+ type: detectMimeType(file.requestedPath) ?? '',
176
+ })
145
177
 
146
178
  let finalFileOptions: FileResponseOptions = { ...fileOptions }
147
179
 
@@ -159,3 +191,20 @@ export function staticFiles(root: string, options: StaticFilesOptions = {}): Mid
159
191
  return next()
160
192
  }
161
193
  }
194
+
195
+ async function resolveContainedPath(
196
+ rootRealPath: string,
197
+ targetPath: string,
198
+ ): Promise<string | null> {
199
+ try {
200
+ let realPath = await fsp.realpath(targetPath)
201
+ return isContainedPath(rootRealPath, realPath) ? realPath : null
202
+ } catch {
203
+ return null
204
+ }
205
+ }
206
+
207
+ function isContainedPath(rootPath: string, targetPath: string): boolean {
208
+ let relativePath = path.relative(rootPath, targetPath)
209
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))
210
+ }