Skip to content

Commit f15f225

Browse files
authored
fix: make static folders with dot such as .well-known allowed (#2929)
This PR intends to address an error logged when opening a DevTools in a Chromium based browser while using Fresh in dev mode. <details> <summary><strong>See error</strong></summary> ``` Error: Processed file resolved outside of static dir C:\Users\<name>\fresh-site\src\static\.well-known\appspecific\com.chrome.devtools.json at MemoryBuildCache.readFile (https://jsr.io/@fresh/core/2.0.0-alpha.29/src/dev/dev_build_cache.ts:81:13) at eventLoopTick (ext:core/01_core.js:178:7) at async freshStaticFiles (https://jsr.io/@fresh/core/2.0.0-alpha.29/src/middlewares/static_files.ts:30:18) at async FreshReqContext.fn (https://jsr.io/@fresh/core/2.0.0-alpha.29/src/middlewares/mod.ts:99:18) at async https://jsr.io/@fresh/core/2.0.0-alpha.29/src/dev/middlewares/error_overlay/middleware.tsx:15:14 at async FreshReqContext.fn (https://jsr.io/@fresh/core/2.0.0-alpha.29/src/middlewares/mod.ts:99:18) at async fn (https://jsr.io/@fresh/core/2.0.0-alpha.29/src/middlewares/mod.ts:99:18) at async https://jsr.io/@fresh/core/2.0.0-alpha.29/src/app.ts:232:16 at async mapped (ext:deno_http/00_serve.ts:407:18) ``` </details> DevTools tries to load a `<root>/static/.well-known/appspecific/com.chrome.devtools.json` file, which incorrectly is flagged as "outside" the static directory, and thus generates errors instead of 404. Maybe hidden folders (starting with `.`) in `/static` should be opt-in though? I'm not sure if anyone puts folders with `.` prefix inside `static/` without intending them to be accessible? - `..` throws errors (outside staticDir) - `.` returns "missing file" (unless `config.allowHiddenFolders` is true) ## Automatic workspace folders I think a follow-up PR could be for the Dev `Builder` to also implement support for [Automatic workspace folders](https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md), by returning JSON content automatically at `.well-known/appspecific/com.chrome.devtools.json`. I think a good first step is to allow `.<folder-name>`, especially since `.well-known/` has [many valid use-cases](https://en.wikipedia.org/wiki/Well-known_URI), and as such is not unreasonable to want to add static files in.
1 parent 50e5bc7 commit f15f225

File tree

5 files changed

+126
-3
lines changed

5 files changed

+126
-3
lines changed

src/build_cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class ProdBuildCache implements BuildCache {
111111
: path.join(base, pathname);
112112

113113
// Check if path resolves outside of intended directory.
114-
if (path.relative(base, filePath).startsWith(".")) {
114+
if (path.relative(base, filePath).startsWith("..")) {
115115
return null;
116116
}
117117

src/build_cache_test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect } from "@std/expect";
2+
import * as path from "@std/path";
3+
import { ProdBuildCache, type StaticFile } from "./build_cache.ts";
4+
import type { ResolvedFreshConfig } from "./mod.ts";
5+
6+
async function getContent(readResult: Promise<StaticFile | null>) {
7+
const res = await readResult;
8+
if (res === null) return null;
9+
if (res.readable instanceof Uint8Array) throw new Error("not implemented");
10+
return new Response(res.readable).text();
11+
}
12+
13+
Deno.test({
14+
name: "ProdBuildCache - should error if reading outside of staticDir",
15+
fn: async () => {
16+
const tmp = await Deno.makeTempDir();
17+
const config: ResolvedFreshConfig = {
18+
root: tmp,
19+
mode: "production",
20+
basePath: "/",
21+
staticDir: path.join(tmp, "static"),
22+
build: {
23+
outDir: path.join(tmp, "dist"),
24+
},
25+
};
26+
await Deno.mkdir(path.join(tmp, "static", ".well-known"), {
27+
recursive: true,
28+
});
29+
await Deno.mkdir(path.join(tmp, "dist", "static"), {
30+
recursive: true,
31+
});
32+
await Promise.all([
33+
Deno.writeTextFile(
34+
path.join(tmp, "dist", "secret-styles.css"),
35+
"SECRET!",
36+
),
37+
Deno.writeTextFile(path.join(tmp, "SECRETS.txt"), "SECRET!"),
38+
Deno.writeTextFile(path.join(tmp, "dist", "static", "styles.css"), "OK"),
39+
Deno.writeTextFile(
40+
path.join(tmp, "static", ".well-known", "foo.txt"),
41+
"OK",
42+
),
43+
]);
44+
const buildCache = new ProdBuildCache(
45+
config,
46+
new Map(),
47+
new Map([
48+
["../secret-styles.css", { generated: true, hash: "SECRET!" }],
49+
["../SECRETS.txt", { generated: false, hash: "SECRET!" }],
50+
["./../secret-styles.css", { generated: true, hash: "SECRET!" }],
51+
["./../SECRETS.txt", { generated: false, hash: "SECRET!" }],
52+
["styles.css", { generated: true, hash: "OK" }],
53+
[".well-known/foo.txt", { generated: false, hash: "OK" }],
54+
["./styles.css", { generated: true, hash: "OK" }],
55+
["./.well-known/foo.txt", { generated: false, hash: "OK" }],
56+
]),
57+
true,
58+
);
59+
60+
const secret1 = getContent(buildCache.readFile("../styles.css"));
61+
const secret2 = getContent(buildCache.readFile("../SECRETS.txt"));
62+
const secret3 = getContent(buildCache.readFile("./../styles.css"));
63+
const secret4 = getContent(buildCache.readFile("./../SECRETS.txt"));
64+
const public1 = getContent(buildCache.readFile("styles.css"));
65+
const public2 = getContent(buildCache.readFile(".well-known/foo.txt"));
66+
const public3 = getContent(buildCache.readFile("./styles.css"));
67+
const public4 = getContent(buildCache.readFile("./.well-known/foo.txt"));
68+
69+
await expect(secret1).resolves.toBe(null);
70+
await expect(secret2).resolves.toBe(null);
71+
await expect(secret3).resolves.toBe(null);
72+
await expect(secret4).resolves.toBe(null);
73+
await expect(public1).resolves.toBe("OK");
74+
await expect(public2).resolves.toBe("OK");
75+
await expect(public3).resolves.toBe("OK");
76+
await expect(public4).resolves.toBe("OK");
77+
},
78+
});

src/dev/dev_build_cache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export class MemoryBuildCache implements DevBuildCache {
7777
let entry = pathname.startsWith("/") ? pathname.slice(1) : pathname;
7878
entry = path.join(this.config.staticDir, entry);
7979
const relative = path.relative(this.config.staticDir, entry);
80-
if (relative.startsWith(".")) {
80+
if (relative.startsWith("..")) {
8181
throw new Error(
8282
`Processed file resolved outside of static dir ${entry}`,
8383
);

src/dev/dev_build_cache_test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { expect } from "@std/expect";
2+
import * as path from "@std/path";
3+
import { MemoryBuildCache } from "./dev_build_cache.ts";
4+
import { FreshFileTransformer } from "./file_transformer.ts";
5+
import { createFakeFs } from "../test_utils.ts";
6+
import type { ResolvedFreshConfig } from "../mod.ts";
7+
8+
Deno.test({
9+
name: "MemoryBuildCache - should error if reading outside of staticDir",
10+
fn: async () => {
11+
const tmp = await Deno.makeTempDir();
12+
const config: ResolvedFreshConfig = {
13+
root: tmp,
14+
mode: "development",
15+
basePath: "/",
16+
staticDir: path.join(tmp, "static"),
17+
build: {
18+
outDir: path.join(tmp, "dist"),
19+
},
20+
};
21+
const fileTransformer = new FreshFileTransformer(createFakeFs({}));
22+
const buildCache = new MemoryBuildCache(
23+
config,
24+
"testing",
25+
fileTransformer,
26+
"latest",
27+
);
28+
29+
const thrown = buildCache.readFile("../SECRETS.txt");
30+
const thrown2 = buildCache.readFile("./../../SECRETS.txt");
31+
const noThrown = buildCache.readFile("styles.css");
32+
const noThrown2 = buildCache.readFile(".well-known/foo.txt");
33+
const noThrown3 = buildCache.readFile("./styles.css");
34+
const noThrown4 = buildCache.readFile("./.well-known/foo.txt");
35+
await buildCache.flush();
36+
37+
const err = "Processed file resolved outside of static dir";
38+
await expect(thrown).rejects.toThrow(err);
39+
await expect(thrown2).rejects.toThrow(err);
40+
await expect(noThrown).resolves.toBe(null);
41+
await expect(noThrown2).resolves.toBe(null);
42+
await expect(noThrown3).resolves.toBe(null);
43+
await expect(noThrown4).resolves.toBe(null);
44+
},
45+
});

src/middlewares/static_files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { trace, tracer } from "../otel.ts";
1010
* Fresh middleware to enable file-system based routing.
1111
* ```ts
1212
* // Enable Fresh static file serving
13-
* app.use(freshStaticFles());
13+
* app.use(staticFiles());
1414
* ```
1515
*/
1616
export function staticFiles<T>(): MiddlewareFn<T> {

0 commit comments

Comments
 (0)