Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
- Fixed issue where MCP server didn't detect if iOS app uses Crashlytics in projects that use `project.pbxproj` (#9515)
- Add logic to synchronize v2 scheduled function timeout with Cloud Schduler's attempt deadline (#9544)
- Prevent deployments of Next.js apps vulnerable to CVE-2025-66478 (#9572)
- Updated Data Connect emulator to v2.17.3:
- Fixed Swift codegen: Include FirebaseCore import in the connector keys file.
- Fixed a bug where debug details of Internal errors were swallowed: https://github.com/firebase/firebase-tools/issues/9508
32 changes: 31 additions & 1 deletion src/frameworks/next/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";
import { copy, mkdirp, pathExists, pathExistsSync, readFile } from "fs-extra";
import { pathToFileURL, parse } from "url";
import { gte } from "semver";
import { gte, coerce } from "semver";
import { IncomingMessage, ServerResponse } from "http";
import * as clc from "colorette";
import { chain } from "stream-chain";
Expand Down Expand Up @@ -58,6 +58,9 @@
whichNextConfigFile,
installEsbuild,
findEsbuildPath,
isUsingAppDirectory,
getNextVersionRaw,
isNextJsVersionVulnerable,
} from "./utils";
import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants";
import type {
Expand Down Expand Up @@ -97,13 +100,13 @@
const DEFAULT_NUMBER_OF_REASONS_TO_LIST = 5;

function getReactVersion(cwd: string): string | undefined {
return findDependency("react-dom", { cwd, omitDev: false })?.version;

Check warning on line 103 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .version on an `any` value

Check warning on line 103 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe return of an `any` typed value
}

/**
* Returns whether this codebase is a Next.js backend.
*/
export async function discover(dir: string) {

Check warning on line 109 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
if (!(await pathExists(join(dir, "package.json")))) return;
const version = getNextVersion(dir);
if (!(await whichNextConfigFile(dir)) && !version) return;
Expand Down Expand Up @@ -160,10 +163,10 @@

const nextBuild = new Promise((resolve, reject) => {
const buildProcess = spawn(cli, ["build"], { cwd: dir, env });
buildProcess.stdout?.on("data", (data) => logger.info(data.toString()));

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

Check warning on line 166 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`
buildProcess.stderr?.on("data", (data) => logger.info(data.toString()));

Check warning on line 167 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 167 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .toString on an `any` value

Check warning on line 167 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`
buildProcess.on("error", (err) => {
reject(new FirebaseError(`Unable to build your Next.js app: ${err}`));

Check warning on line 169 in src/frameworks/next/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Invalid type "Error" of template literal expression
});
buildProcess.on("exit", (code) => {
resolve(code);
Expand Down Expand Up @@ -339,6 +342,33 @@

const wantsBackend = reasonsForBackend.size > 0;

if (wantsBackend && isUsingAppDirectory(join(dir, distDir))) {
const nextVersion = getNextVersionRaw(dir);
if (nextVersion && isNextJsVersionVulnerable(nextVersion)) {
let message =
`Next.js version ${nextVersion} is vulnerable to CVE-2025-66478.\n` +
`Please upgrade to a patched version: `;

const { major } = coerce(nextVersion) || {};
if (major === 16) {
message += "16.0.7+.";
} else if (major === 15) {
message += "15.0.5+, 15.1.9+, 15.2.6+, 15.3.6+, 15.4.8+, or 15.5.7+.";
} else if (major === 14) {
message += "downgrade to a stable Next.js 14.x release.";
} else {
// Fallback for unexpected cases
message +=
"15.0.5+, 15.1.9+, 15.2.6+, 15.3.6+, 15.4.8+, 15.5.7+, 16.0.7+ " +
"or downgrade to a stable Next.js 14.x release if using canary.";
}

message += `\nSee https://nextjs.org/blog/CVE-2025-66478 for more details.`;

throw new FirebaseError(message);
}
}

if (wantsBackend) {
logger.info("Building a Cloud Function to run this application. This is needed due to:");
for (const reason of Array.from(reasonsForBackend).slice(
Expand Down
150 changes: 150 additions & 0 deletions src/frameworks/next/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import {
getAppMetadataFromMetaFiles,
isUsingNextImageInAppDirectory,
getNextVersion,
getNextVersionRaw,
getRoutesWithServerAction,
findEsbuildPath,
installEsbuild,
isNextJsVersionVulnerable,
} from "./utils";

import * as frameworksUtils from "../utils";
Expand Down Expand Up @@ -556,6 +558,30 @@ describe("Next.js utils", () => {
});
});

describe("getNextVersionRaw", () => {
let sandbox: sinon.SinonSandbox;
beforeEach(() => (sandbox = sinon.createSandbox()));
afterEach(() => sandbox.restore());

it("should get version", () => {
sandbox.stub(frameworksUtils, "findDependency").returns({ version: "13.4.10" });

expect(getNextVersionRaw("")).to.equal("13.4.10");
});

it("should return exact version including canary", () => {
sandbox.stub(frameworksUtils, "findDependency").returns({ version: "13.4.10-canary.0" });

expect(getNextVersionRaw("")).to.equal("13.4.10-canary.0");
});

it("should return undefined if unable to get version", () => {
sandbox.stub(frameworksUtils, "findDependency").returns(undefined);

expect(getNextVersionRaw("")).to.be.undefined;
});
});

describe("getRoutesWithServerAction", () => {
it("should get routes with server action", () => {
expect(
Expand Down Expand Up @@ -651,4 +677,128 @@ describe("Next.js utils", () => {
}
});
});

describe("isNextJsVersionVulnerable", () => {
describe("vulnerable versions", () => {
it("should block vulnerable 15.0.x versions (< 15.0.5)", () => {
expect(isNextJsVersionVulnerable("15.0.4")).to.be.true;
expect(isNextJsVersionVulnerable("15.0.0")).to.be.true;
expect(isNextJsVersionVulnerable("15.0.0-rc.1")).to.be.true;
expect(isNextJsVersionVulnerable("15.0.0-canary.205")).to.be.true;
});

it("should block vulnerable 15.1.x versions (< 15.1.9)", () => {
expect(isNextJsVersionVulnerable("15.1.8")).to.be.true;
expect(isNextJsVersionVulnerable("15.1.0")).to.be.true;
expect(isNextJsVersionVulnerable("15.1.1-canary.27")).to.be.true;
});

it("should block vulnerable 15.2.x versions (< 15.2.6)", () => {
expect(isNextJsVersionVulnerable("15.2.5")).to.be.true;
expect(isNextJsVersionVulnerable("15.2.0-canary.77")).to.be.true;
});

it("should block vulnerable 15.3.x versions (< 15.3.6)", () => {
expect(isNextJsVersionVulnerable("15.3.5")).to.be.true;
expect(isNextJsVersionVulnerable("15.3.0-canary.46")).to.be.true;
});

it("should block vulnerable 15.4.x versions (< 15.4.8)", () => {
expect(isNextJsVersionVulnerable("15.4.7")).to.be.true;
expect(isNextJsVersionVulnerable("15.4.2-canary.56")).to.be.true;
expect(isNextJsVersionVulnerable("15.4.0-canary.130")).to.be.true;
});

it("should block vulnerable 15.5.x versions (< 15.5.7)", () => {
expect(isNextJsVersionVulnerable("15.5.6")).to.be.true;
expect(isNextJsVersionVulnerable("15.5.1-canary.39")).to.be.true;
});

it("should block vulnerable 16.0.x versions (< 16.0.7)", () => {
expect(isNextJsVersionVulnerable("16.0.6")).to.be.true;
expect(isNextJsVersionVulnerable("16.0.0-beta.0")).to.be.true;
expect(isNextJsVersionVulnerable("16.0.0-canary.18")).to.be.true;
expect(isNextJsVersionVulnerable("16.0.2-canary.34")).to.be.true;
});

it("should block vulnerable 14.x canary versions (>= 14.3.0-canary.77)", () => {
expect(isNextJsVersionVulnerable("14.3.0-canary.77")).to.be.true;
expect(isNextJsVersionVulnerable("14.3.0-canary.87")).to.be.true;
});

it("should treat pre-releases of patched versions as vulnerable (conservative)", () => {
expect(isNextJsVersionVulnerable("15.0.5-canary.1")).to.be.true;
});

it("should block versions with build metadata if base is vulnerable", () => {
expect(isNextJsVersionVulnerable("15.0.4+build123")).to.be.true;
});
});

describe("safe versions", () => {
it("should allow patched 15.0.x versions (>= 15.0.5)", () => {
expect(isNextJsVersionVulnerable("15.0.5")).to.be.false;
expect(isNextJsVersionVulnerable("15.0.6")).to.be.false;
});

it("should allow patched 15.1.x versions (>= 15.1.9)", () => {
expect(isNextJsVersionVulnerable("15.1.9")).to.be.false;
});

it("should allow patched 15.2.x versions (>= 15.2.6)", () => {
expect(isNextJsVersionVulnerable("15.2.6")).to.be.false;
});

it("should allow patched 15.3.x versions (>= 15.3.6)", () => {
expect(isNextJsVersionVulnerable("15.3.6")).to.be.false;
});

it("should allow patched 15.4.x versions (>= 15.4.8)", () => {
expect(isNextJsVersionVulnerable("15.4.8")).to.be.false;
});

it("should allow patched 15.5.x versions (>= 15.5.7)", () => {
expect(isNextJsVersionVulnerable("15.5.7")).to.be.false;
});

it("should allow newer minor versions (e.g. 15.6.x)", () => {
expect(isNextJsVersionVulnerable("15.6.0-canary.57")).to.be.false;
});

it("should allow patched 16.0.x versions (>= 16.0.7)", () => {
expect(isNextJsVersionVulnerable("16.0.7")).to.be.false;
});

it("should allow newer 16.x minor versions (e.g. 16.1.x)", () => {
expect(isNextJsVersionVulnerable("16.1.0-canary.12")).to.be.false;
});

it("should allow safe 14.x canary versions (< 14.3.0-canary.77)", () => {
expect(isNextJsVersionVulnerable("14.3.0-canary.76")).to.be.false;
expect(isNextJsVersionVulnerable("14.3.0-canary.43")).to.be.false;
expect(isNextJsVersionVulnerable("14.2.0-canary.67")).to.be.false;
});

it("should allow stable 14.x versions (not vulnerable)", () => {
expect(isNextJsVersionVulnerable("14.3.0")).to.be.false;
expect(isNextJsVersionVulnerable("14.2.33")).to.be.false;
expect(isNextJsVersionVulnerable("14.1.4")).to.be.false;
});

it("should allow unaffected older versions", () => {
expect(isNextJsVersionVulnerable("13.5.11")).to.be.false;
expect(isNextJsVersionVulnerable("12.3.7")).to.be.false;
});

it("should allow versions with build metadata if base is safe", () => {
expect(isNextJsVersionVulnerable("15.0.5+build123")).to.be.false;
});

it("should return false for invalid versions (fail open)", () => {
expect(isNextJsVersionVulnerable("invalid-version")).to.be.false;
expect(isNextJsVersionVulnerable("")).to.be.false;
expect(isNextJsVersionVulnerable(undefined as any)).to.be.false;
});
});
});
});
59 changes: 58 additions & 1 deletion src/frameworks/next/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { basename, extname, join, posix, sep, resolve, dirname } from "path";
import { readFile } from "fs/promises";
import { glob, sync as globSync } from "glob";
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";
import { coerce, satisfies } from "semver";
import { coerce, satisfies, lt, gte, prerelease, parse } from "semver";

import { findDependency, isUrl, readJSON } from "../utils";
import type {
Expand Down Expand Up @@ -426,6 +426,14 @@ export function getNextVersion(cwd: string): string | undefined {
return nextVersionSemver.toString();
}

/**
* Get the raw Next.js version from the project.
*/
export function getNextVersionRaw(cwd: string): string | undefined {
const dependency = findDependency("next", { cwd, depth: 0, omitDev: false });
return dependency?.version;
}

/**
* Whether the Next.js project has a static `not-found` page in the app directory.
*
Expand Down Expand Up @@ -553,3 +561,52 @@ export function installEsbuild(version: string): void {
}
}
}

/**
* Check if the Next.js version is vulnerable to CVE-2025-66478.
*
* Vulnerable versions:
* - 15.0.x < 15.0.5
* - 15.1.x < 15.1.9
* - 15.2.x < 15.2.6
* - 15.3.x < 15.3.6
* - 15.4.x < 15.4.8
* - 15.5.x < 15.5.7
* - 16.0.x < 16.0.7
* - 14.x canary >= 14.3.0-canary.77
*
* @see https://nextjs.org/blog/CVE-2025-66478
* @see https://www.cve.org/CVERecord?id=CVE-2025-55182
* @see https://github.com/vercel/next.js/security/advisories/GHSA-9qr9-h5gf-34mp
*/
export function isNextJsVersionVulnerable(versionStr: string): boolean {
const v = parse(versionStr);
if (!v) return false;

if (v.major === 15) {
if (v.minor === 0) return lt(versionStr, "15.0.5");
if (v.minor === 1) return lt(versionStr, "15.1.9");
if (v.minor === 2) return lt(versionStr, "15.2.6");
if (v.minor === 3) return lt(versionStr, "15.3.6");
if (v.minor === 4) return lt(versionStr, "15.4.8");
if (v.minor === 5) return lt(versionStr, "15.5.7");
// Assume newer minor versions (e.g. 15.6.x) are safe as they should include the fix.
return false;
}

if (v.major === 16) {
if (v.minor === 0) return lt(versionStr, "16.0.7");
return false;
}

if (v.major === 14) {
const pre = prerelease(versionStr);
if (pre && pre.includes("canary")) {
if (gte(versionStr, "14.3.0-canary.77")) {
return true;
}
}
}

return false;
}
Loading