Skip to content

Commit 763951a

Browse files
leoortizzjoehan
andauthored
prevent deployments of Next.js apps vulnerable to CVE-2025-66478 (#9572)
* prevent deployments of Next.js apps vulnerable to CVE-2025-66478 * Update CHANGELOG to reference CVE-2025-66478 prevention in Next.js deployments (#9572) * Refactor error message for Next.js CVE-2025-66478 vulnerability check Updated the error handling to provide a more detailed and version-specific message when a vulnerable Next.js version is detected. The message now specifies the required patched versions based on the major version of Next.js, enhancing clarity for users on how to resolve the issue. * Enhance Next.js vulnerability check documentation Added references to CVE-2025-66478 and CVE-2025-55182 in the documentation for the Next.js version vulnerability check function. This provides users with direct links to relevant security advisories, improving awareness and guidance on addressing potential vulnerabilities. * format * update next.js version in webframeworks-deploy-tests * use semver for error message checks * remove unnecessary comments * Revert "update next.js version in webframeworks-deploy-tests" This reverts commit 22857f4. --------- Co-authored-by: Joe Hanley <joehanley@google.com>
1 parent c23ac23 commit 763951a

File tree

4 files changed

+240
-2
lines changed

4 files changed

+240
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
- Fixed issue where MCP server didn't detect if iOS app uses Crashlytics in projects that use `project.pbxproj` (#9515)
22
- Add logic to synchronize v2 scheduled function timeout with Cloud Schduler's attempt deadline (#9544)
3+
- Prevent deployments of Next.js apps vulnerable to CVE-2025-66478 (#9572)
34
- Updated Data Connect emulator to v2.17.3:
45
- Fixed Swift codegen: Include FirebaseCore import in the connector keys file.
56
- Fixed a bug where debug details of Internal errors were swallowed: https://github.com/firebase/firebase-tools/issues/9508

src/frameworks/next/index.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { DomainLocale } from "next/dist/server/config";
88
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";
99
import { copy, mkdirp, pathExists, pathExistsSync, readFile } from "fs-extra";
1010
import { pathToFileURL, parse } from "url";
11-
import { gte } from "semver";
11+
import { gte, coerce } from "semver";
1212
import { IncomingMessage, ServerResponse } from "http";
1313
import * as clc from "colorette";
1414
import { chain } from "stream-chain";
@@ -58,6 +58,9 @@ import {
5858
whichNextConfigFile,
5959
installEsbuild,
6060
findEsbuildPath,
61+
isUsingAppDirectory,
62+
getNextVersionRaw,
63+
isNextJsVersionVulnerable,
6164
} from "./utils";
6265
import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants";
6366
import type {
@@ -339,6 +342,33 @@ export async function build(
339342

340343
const wantsBackend = reasonsForBackend.size > 0;
341344

345+
if (wantsBackend && isUsingAppDirectory(join(dir, distDir))) {
346+
const nextVersion = getNextVersionRaw(dir);
347+
if (nextVersion && isNextJsVersionVulnerable(nextVersion)) {
348+
let message =
349+
`Next.js version ${nextVersion} is vulnerable to CVE-2025-66478.\n` +
350+
`Please upgrade to a patched version: `;
351+
352+
const { major } = coerce(nextVersion) || {};
353+
if (major === 16) {
354+
message += "16.0.7+.";
355+
} else if (major === 15) {
356+
message += "15.0.5+, 15.1.9+, 15.2.6+, 15.3.6+, 15.4.8+, or 15.5.7+.";
357+
} else if (major === 14) {
358+
message += "downgrade to a stable Next.js 14.x release.";
359+
} else {
360+
// Fallback for unexpected cases
361+
message +=
362+
"15.0.5+, 15.1.9+, 15.2.6+, 15.3.6+, 15.4.8+, 15.5.7+, 16.0.7+ " +
363+
"or downgrade to a stable Next.js 14.x release if using canary.";
364+
}
365+
366+
message += `\nSee https://nextjs.org/blog/CVE-2025-66478 for more details.`;
367+
368+
throw new FirebaseError(message);
369+
}
370+
}
371+
342372
if (wantsBackend) {
343373
logger.info("Building a Cloud Function to run this application. This is needed due to:");
344374
for (const reason of Array.from(reasonsForBackend).slice(

src/frameworks/next/utils.spec.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ import {
3535
getAppMetadataFromMetaFiles,
3636
isUsingNextImageInAppDirectory,
3737
getNextVersion,
38+
getNextVersionRaw,
3839
getRoutesWithServerAction,
3940
findEsbuildPath,
4041
installEsbuild,
42+
isNextJsVersionVulnerable,
4143
} from "./utils";
4244

4345
import * as frameworksUtils from "../utils";
@@ -556,6 +558,30 @@ describe("Next.js utils", () => {
556558
});
557559
});
558560

561+
describe("getNextVersionRaw", () => {
562+
let sandbox: sinon.SinonSandbox;
563+
beforeEach(() => (sandbox = sinon.createSandbox()));
564+
afterEach(() => sandbox.restore());
565+
566+
it("should get version", () => {
567+
sandbox.stub(frameworksUtils, "findDependency").returns({ version: "13.4.10" });
568+
569+
expect(getNextVersionRaw("")).to.equal("13.4.10");
570+
});
571+
572+
it("should return exact version including canary", () => {
573+
sandbox.stub(frameworksUtils, "findDependency").returns({ version: "13.4.10-canary.0" });
574+
575+
expect(getNextVersionRaw("")).to.equal("13.4.10-canary.0");
576+
});
577+
578+
it("should return undefined if unable to get version", () => {
579+
sandbox.stub(frameworksUtils, "findDependency").returns(undefined);
580+
581+
expect(getNextVersionRaw("")).to.be.undefined;
582+
});
583+
});
584+
559585
describe("getRoutesWithServerAction", () => {
560586
it("should get routes with server action", () => {
561587
expect(
@@ -651,4 +677,128 @@ describe("Next.js utils", () => {
651677
}
652678
});
653679
});
680+
681+
describe("isNextJsVersionVulnerable", () => {
682+
describe("vulnerable versions", () => {
683+
it("should block vulnerable 15.0.x versions (< 15.0.5)", () => {
684+
expect(isNextJsVersionVulnerable("15.0.4")).to.be.true;
685+
expect(isNextJsVersionVulnerable("15.0.0")).to.be.true;
686+
expect(isNextJsVersionVulnerable("15.0.0-rc.1")).to.be.true;
687+
expect(isNextJsVersionVulnerable("15.0.0-canary.205")).to.be.true;
688+
});
689+
690+
it("should block vulnerable 15.1.x versions (< 15.1.9)", () => {
691+
expect(isNextJsVersionVulnerable("15.1.8")).to.be.true;
692+
expect(isNextJsVersionVulnerable("15.1.0")).to.be.true;
693+
expect(isNextJsVersionVulnerable("15.1.1-canary.27")).to.be.true;
694+
});
695+
696+
it("should block vulnerable 15.2.x versions (< 15.2.6)", () => {
697+
expect(isNextJsVersionVulnerable("15.2.5")).to.be.true;
698+
expect(isNextJsVersionVulnerable("15.2.0-canary.77")).to.be.true;
699+
});
700+
701+
it("should block vulnerable 15.3.x versions (< 15.3.6)", () => {
702+
expect(isNextJsVersionVulnerable("15.3.5")).to.be.true;
703+
expect(isNextJsVersionVulnerable("15.3.0-canary.46")).to.be.true;
704+
});
705+
706+
it("should block vulnerable 15.4.x versions (< 15.4.8)", () => {
707+
expect(isNextJsVersionVulnerable("15.4.7")).to.be.true;
708+
expect(isNextJsVersionVulnerable("15.4.2-canary.56")).to.be.true;
709+
expect(isNextJsVersionVulnerable("15.4.0-canary.130")).to.be.true;
710+
});
711+
712+
it("should block vulnerable 15.5.x versions (< 15.5.7)", () => {
713+
expect(isNextJsVersionVulnerable("15.5.6")).to.be.true;
714+
expect(isNextJsVersionVulnerable("15.5.1-canary.39")).to.be.true;
715+
});
716+
717+
it("should block vulnerable 16.0.x versions (< 16.0.7)", () => {
718+
expect(isNextJsVersionVulnerable("16.0.6")).to.be.true;
719+
expect(isNextJsVersionVulnerable("16.0.0-beta.0")).to.be.true;
720+
expect(isNextJsVersionVulnerable("16.0.0-canary.18")).to.be.true;
721+
expect(isNextJsVersionVulnerable("16.0.2-canary.34")).to.be.true;
722+
});
723+
724+
it("should block vulnerable 14.x canary versions (>= 14.3.0-canary.77)", () => {
725+
expect(isNextJsVersionVulnerable("14.3.0-canary.77")).to.be.true;
726+
expect(isNextJsVersionVulnerable("14.3.0-canary.87")).to.be.true;
727+
});
728+
729+
it("should treat pre-releases of patched versions as vulnerable (conservative)", () => {
730+
expect(isNextJsVersionVulnerable("15.0.5-canary.1")).to.be.true;
731+
});
732+
733+
it("should block versions with build metadata if base is vulnerable", () => {
734+
expect(isNextJsVersionVulnerable("15.0.4+build123")).to.be.true;
735+
});
736+
});
737+
738+
describe("safe versions", () => {
739+
it("should allow patched 15.0.x versions (>= 15.0.5)", () => {
740+
expect(isNextJsVersionVulnerable("15.0.5")).to.be.false;
741+
expect(isNextJsVersionVulnerable("15.0.6")).to.be.false;
742+
});
743+
744+
it("should allow patched 15.1.x versions (>= 15.1.9)", () => {
745+
expect(isNextJsVersionVulnerable("15.1.9")).to.be.false;
746+
});
747+
748+
it("should allow patched 15.2.x versions (>= 15.2.6)", () => {
749+
expect(isNextJsVersionVulnerable("15.2.6")).to.be.false;
750+
});
751+
752+
it("should allow patched 15.3.x versions (>= 15.3.6)", () => {
753+
expect(isNextJsVersionVulnerable("15.3.6")).to.be.false;
754+
});
755+
756+
it("should allow patched 15.4.x versions (>= 15.4.8)", () => {
757+
expect(isNextJsVersionVulnerable("15.4.8")).to.be.false;
758+
});
759+
760+
it("should allow patched 15.5.x versions (>= 15.5.7)", () => {
761+
expect(isNextJsVersionVulnerable("15.5.7")).to.be.false;
762+
});
763+
764+
it("should allow newer minor versions (e.g. 15.6.x)", () => {
765+
expect(isNextJsVersionVulnerable("15.6.0-canary.57")).to.be.false;
766+
});
767+
768+
it("should allow patched 16.0.x versions (>= 16.0.7)", () => {
769+
expect(isNextJsVersionVulnerable("16.0.7")).to.be.false;
770+
});
771+
772+
it("should allow newer 16.x minor versions (e.g. 16.1.x)", () => {
773+
expect(isNextJsVersionVulnerable("16.1.0-canary.12")).to.be.false;
774+
});
775+
776+
it("should allow safe 14.x canary versions (< 14.3.0-canary.77)", () => {
777+
expect(isNextJsVersionVulnerable("14.3.0-canary.76")).to.be.false;
778+
expect(isNextJsVersionVulnerable("14.3.0-canary.43")).to.be.false;
779+
expect(isNextJsVersionVulnerable("14.2.0-canary.67")).to.be.false;
780+
});
781+
782+
it("should allow stable 14.x versions (not vulnerable)", () => {
783+
expect(isNextJsVersionVulnerable("14.3.0")).to.be.false;
784+
expect(isNextJsVersionVulnerable("14.2.33")).to.be.false;
785+
expect(isNextJsVersionVulnerable("14.1.4")).to.be.false;
786+
});
787+
788+
it("should allow unaffected older versions", () => {
789+
expect(isNextJsVersionVulnerable("13.5.11")).to.be.false;
790+
expect(isNextJsVersionVulnerable("12.3.7")).to.be.false;
791+
});
792+
793+
it("should allow versions with build metadata if base is safe", () => {
794+
expect(isNextJsVersionVulnerable("15.0.5+build123")).to.be.false;
795+
});
796+
797+
it("should return false for invalid versions (fail open)", () => {
798+
expect(isNextJsVersionVulnerable("invalid-version")).to.be.false;
799+
expect(isNextJsVersionVulnerable("")).to.be.false;
800+
expect(isNextJsVersionVulnerable(undefined as any)).to.be.false;
801+
});
802+
});
803+
});
654804
});

src/frameworks/next/utils.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { basename, extname, join, posix, sep, resolve, dirname } from "path";
44
import { readFile } from "fs/promises";
55
import { glob, sync as globSync } from "glob";
66
import type { PagesManifest } from "next/dist/build/webpack/plugins/pages-manifest-plugin";
7-
import { coerce, satisfies } from "semver";
7+
import { coerce, satisfies, lt, gte, prerelease, parse } from "semver";
88

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

429+
/**
430+
* Get the raw Next.js version from the project.
431+
*/
432+
export function getNextVersionRaw(cwd: string): string | undefined {
433+
const dependency = findDependency("next", { cwd, depth: 0, omitDev: false });
434+
return dependency?.version;
435+
}
436+
429437
/**
430438
* Whether the Next.js project has a static `not-found` page in the app directory.
431439
*
@@ -553,3 +561,52 @@ export function installEsbuild(version: string): void {
553561
}
554562
}
555563
}
564+
565+
/**
566+
* Check if the Next.js version is vulnerable to CVE-2025-66478.
567+
*
568+
* Vulnerable versions:
569+
* - 15.0.x < 15.0.5
570+
* - 15.1.x < 15.1.9
571+
* - 15.2.x < 15.2.6
572+
* - 15.3.x < 15.3.6
573+
* - 15.4.x < 15.4.8
574+
* - 15.5.x < 15.5.7
575+
* - 16.0.x < 16.0.7
576+
* - 14.x canary >= 14.3.0-canary.77
577+
*
578+
* @see https://nextjs.org/blog/CVE-2025-66478
579+
* @see https://www.cve.org/CVERecord?id=CVE-2025-55182
580+
* @see https://github.com/vercel/next.js/security/advisories/GHSA-9qr9-h5gf-34mp
581+
*/
582+
export function isNextJsVersionVulnerable(versionStr: string): boolean {
583+
const v = parse(versionStr);
584+
if (!v) return false;
585+
586+
if (v.major === 15) {
587+
if (v.minor === 0) return lt(versionStr, "15.0.5");
588+
if (v.minor === 1) return lt(versionStr, "15.1.9");
589+
if (v.minor === 2) return lt(versionStr, "15.2.6");
590+
if (v.minor === 3) return lt(versionStr, "15.3.6");
591+
if (v.minor === 4) return lt(versionStr, "15.4.8");
592+
if (v.minor === 5) return lt(versionStr, "15.5.7");
593+
// Assume newer minor versions (e.g. 15.6.x) are safe as they should include the fix.
594+
return false;
595+
}
596+
597+
if (v.major === 16) {
598+
if (v.minor === 0) return lt(versionStr, "16.0.7");
599+
return false;
600+
}
601+
602+
if (v.major === 14) {
603+
const pre = prerelease(versionStr);
604+
if (pre && pre.includes("canary")) {
605+
if (gte(versionStr, "14.3.0-canary.77")) {
606+
return true;
607+
}
608+
}
609+
}
610+
611+
return false;
612+
}

0 commit comments

Comments
 (0)