From 9aa8e20c9f51b07b17fed1d8cb5632bebff26021 Mon Sep 17 00:00:00 2001 From: winklemad Date: Wed, 22 Jul 2026 08:36:36 +0530 Subject: [PATCH 1/2] fix(config): allow two dots in path segments --- src/config.spec.ts | 12 ++++++++++++ src/config.ts | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/config.spec.ts b/src/config.spec.ts index b4ff64f751c..9081a91684c 100644 --- a/src/config.spec.ts +++ b/src/config.spec.ts @@ -43,6 +43,18 @@ describe("Config", () => { const relativePath = "something"; expect(config.path(relativePath)).to.eq(path.join(SIMPLE_CONFIG_DIR, relativePath)); }); + it("should allow path segments containing two dots", () => { + const publicDir = "foo..bar"; + const config = new Config({ hosting: { public: publicDir } }, { cwd: SIMPLE_CONFIG_DIR }); + const configuredPublicDir = config.get("hosting.public") as string; + expect(config.path(configuredPublicDir)).to.eq(path.join(SIMPLE_CONFIG_DIR, publicDir)); + }); + it("should reject parent directory traversal", () => { + const config = new Config({}, { cwd: SIMPLE_CONFIG_DIR }); + for (const relativePath of ["..", path.join("..", "outside")]) { + expect(() => config.path(relativePath)).to.throw("is outside of project directory"); + } + }); }); describe("#parseFile", () => { diff --git a/src/config.ts b/src/config.ts index 31febdf8189..94a12eda10b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -204,7 +204,8 @@ export class Config { return pathName; } const outPath = path.normalize(path.join(this.projectDir, pathName)); - if (path.relative(this.projectDir, outPath).includes("..")) { + const relativePath = path.relative(this.projectDir, outPath); + if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) { throw new FirebaseError(clc.bold(pathName) + " is outside of project directory", { exit: 1 }); } return outPath; From 6cde39a0ead268a8e3a5ce7c818744cef8329253 Mon Sep 17 00:00:00 2001 From: winklemad Date: Wed, 22 Jul 2026 08:59:11 +0530 Subject: [PATCH 2/2] fix(config): reject absolute relative paths --- src/config.spec.ts | 10 ++++++++++ src/config.ts | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/config.spec.ts b/src/config.spec.ts index 9081a91684c..97d88820206 100644 --- a/src/config.spec.ts +++ b/src/config.spec.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import * as os from "os"; import { expect } from "chai"; +import * as sinon from "sinon"; import { Config } from "./config"; import { FIREBASE_JSON_PATH as VALID_CONFIG_PATH } from "./test/fixtures/valid-config"; @@ -33,6 +34,10 @@ describe("Config", () => { }); describe("#path", () => { + afterEach(() => { + sinon.restore(); + }); + it("should skip an absolute path", () => { const config = new Config({}, { cwd: SIMPLE_CONFIG_DIR }); const absPath = "/Users/something"; @@ -55,6 +60,11 @@ describe("Config", () => { expect(() => config.path(relativePath)).to.throw("is outside of project directory"); } }); + it("should reject an absolute result from path.relative", () => { + const config = new Config({}, { cwd: SIMPLE_CONFIG_DIR }); + sinon.stub(path, "relative").returns(path.resolve("outside")); + expect(() => config.path("inside")).to.throw("is outside of project directory"); + }); }); describe("#parseFile", () => { diff --git a/src/config.ts b/src/config.ts index 94a12eda10b..2f747dcbf41 100644 --- a/src/config.ts +++ b/src/config.ts @@ -205,7 +205,11 @@ export class Config { } const outPath = path.normalize(path.join(this.projectDir, pathName)); const relativePath = path.relative(this.projectDir, outPath); - if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) { + if ( + path.isAbsolute(relativePath) || + relativePath === ".." || + relativePath.startsWith(`..${path.sep}`) + ) { throw new FirebaseError(clc.bold(pathName) + " is outside of project directory", { exit: 1 }); } return outPath;