diff --git a/src/build_cache.ts b/src/build_cache.ts index ada90e00d15..709ab36fef3 100644 --- a/src/build_cache.ts +++ b/src/build_cache.ts @@ -111,7 +111,7 @@ export class ProdBuildCache implements BuildCache { : path.join(base, pathname); // Check if path resolves outside of intended directory. - if (path.relative(base, filePath).startsWith(".")) { + if (path.relative(base, filePath).startsWith("..")) { return null; } diff --git a/src/build_cache_test.ts b/src/build_cache_test.ts new file mode 100644 index 00000000000..39cc47469fb --- /dev/null +++ b/src/build_cache_test.ts @@ -0,0 +1,78 @@ +import { expect } from "@std/expect"; +import * as path from "@std/path"; +import { ProdBuildCache, type StaticFile } from "./build_cache.ts"; +import type { ResolvedFreshConfig } from "./mod.ts"; + +async function getContent(readResult: Promise) { + const res = await readResult; + if (res === null) return null; + if (res.readable instanceof Uint8Array) throw new Error("not implemented"); + return new Response(res.readable).text(); +} + +Deno.test({ + name: "ProdBuildCache - should error if reading outside of staticDir", + fn: async () => { + const tmp = await Deno.makeTempDir(); + const config: ResolvedFreshConfig = { + root: tmp, + mode: "production", + basePath: "/", + staticDir: path.join(tmp, "static"), + build: { + outDir: path.join(tmp, "dist"), + }, + }; + await Deno.mkdir(path.join(tmp, "static", ".well-known"), { + recursive: true, + }); + await Deno.mkdir(path.join(tmp, "dist", "static"), { + recursive: true, + }); + await Promise.all([ + Deno.writeTextFile( + path.join(tmp, "dist", "secret-styles.css"), + "SECRET!", + ), + Deno.writeTextFile(path.join(tmp, "SECRETS.txt"), "SECRET!"), + Deno.writeTextFile(path.join(tmp, "dist", "static", "styles.css"), "OK"), + Deno.writeTextFile( + path.join(tmp, "static", ".well-known", "foo.txt"), + "OK", + ), + ]); + const buildCache = new ProdBuildCache( + config, + new Map(), + new Map([ + ["../secret-styles.css", { generated: true, hash: "SECRET!" }], + ["../SECRETS.txt", { generated: false, hash: "SECRET!" }], + ["./../secret-styles.css", { generated: true, hash: "SECRET!" }], + ["./../SECRETS.txt", { generated: false, hash: "SECRET!" }], + ["styles.css", { generated: true, hash: "OK" }], + [".well-known/foo.txt", { generated: false, hash: "OK" }], + ["./styles.css", { generated: true, hash: "OK" }], + ["./.well-known/foo.txt", { generated: false, hash: "OK" }], + ]), + true, + ); + + const secret1 = getContent(buildCache.readFile("../styles.css")); + const secret2 = getContent(buildCache.readFile("../SECRETS.txt")); + const secret3 = getContent(buildCache.readFile("./../styles.css")); + const secret4 = getContent(buildCache.readFile("./../SECRETS.txt")); + const public1 = getContent(buildCache.readFile("styles.css")); + const public2 = getContent(buildCache.readFile(".well-known/foo.txt")); + const public3 = getContent(buildCache.readFile("./styles.css")); + const public4 = getContent(buildCache.readFile("./.well-known/foo.txt")); + + await expect(secret1).resolves.toBe(null); + await expect(secret2).resolves.toBe(null); + await expect(secret3).resolves.toBe(null); + await expect(secret4).resolves.toBe(null); + await expect(public1).resolves.toBe("OK"); + await expect(public2).resolves.toBe("OK"); + await expect(public3).resolves.toBe("OK"); + await expect(public4).resolves.toBe("OK"); + }, +}); diff --git a/src/dev/dev_build_cache.ts b/src/dev/dev_build_cache.ts index dfe3ebc7f6c..358addc1bc3 100644 --- a/src/dev/dev_build_cache.ts +++ b/src/dev/dev_build_cache.ts @@ -77,7 +77,7 @@ export class MemoryBuildCache implements DevBuildCache { let entry = pathname.startsWith("/") ? pathname.slice(1) : pathname; entry = path.join(this.config.staticDir, entry); const relative = path.relative(this.config.staticDir, entry); - if (relative.startsWith(".")) { + if (relative.startsWith("..")) { throw new Error( `Processed file resolved outside of static dir ${entry}`, ); diff --git a/src/dev/dev_build_cache_test.ts b/src/dev/dev_build_cache_test.ts new file mode 100644 index 00000000000..8f740fd05c0 --- /dev/null +++ b/src/dev/dev_build_cache_test.ts @@ -0,0 +1,45 @@ +import { expect } from "@std/expect"; +import * as path from "@std/path"; +import { MemoryBuildCache } from "./dev_build_cache.ts"; +import { FreshFileTransformer } from "./file_transformer.ts"; +import { createFakeFs } from "../test_utils.ts"; +import type { ResolvedFreshConfig } from "../mod.ts"; + +Deno.test({ + name: "MemoryBuildCache - should error if reading outside of staticDir", + fn: async () => { + const tmp = await Deno.makeTempDir(); + const config: ResolvedFreshConfig = { + root: tmp, + mode: "development", + basePath: "/", + staticDir: path.join(tmp, "static"), + build: { + outDir: path.join(tmp, "dist"), + }, + }; + const fileTransformer = new FreshFileTransformer(createFakeFs({})); + const buildCache = new MemoryBuildCache( + config, + "testing", + fileTransformer, + "latest", + ); + + const thrown = buildCache.readFile("../SECRETS.txt"); + const thrown2 = buildCache.readFile("./../../SECRETS.txt"); + const noThrown = buildCache.readFile("styles.css"); + const noThrown2 = buildCache.readFile(".well-known/foo.txt"); + const noThrown3 = buildCache.readFile("./styles.css"); + const noThrown4 = buildCache.readFile("./.well-known/foo.txt"); + await buildCache.flush(); + + const err = "Processed file resolved outside of static dir"; + await expect(thrown).rejects.toThrow(err); + await expect(thrown2).rejects.toThrow(err); + await expect(noThrown).resolves.toBe(null); + await expect(noThrown2).resolves.toBe(null); + await expect(noThrown3).resolves.toBe(null); + await expect(noThrown4).resolves.toBe(null); + }, +}); diff --git a/src/middlewares/static_files.ts b/src/middlewares/static_files.ts index 6e82a177de6..93dc9135bec 100644 --- a/src/middlewares/static_files.ts +++ b/src/middlewares/static_files.ts @@ -10,7 +10,7 @@ import { trace, tracer } from "../otel.ts"; * Fresh middleware to enable file-system based routing. * ```ts * // Enable Fresh static file serving - * app.use(freshStaticFles()); + * app.use(staticFiles()); * ``` */ export function staticFiles(): MiddlewareFn {