vinext 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/client/entry.js +1 -15
- package/dist/client/entry.js.map +1 -1
- package/dist/client/validate-module-path.d.ts +15 -0
- package/dist/client/validate-module-path.d.ts.map +1 -0
- package/dist/client/validate-module-path.js +31 -0
- package/dist/client/validate-module-path.js.map +1 -0
- package/dist/config/config-matchers.d.ts +20 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +185 -36
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/next-config.d.ts +4 -0
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +20 -12
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +173 -155
- package/dist/index.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +2 -1
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-dev-server.d.ts +2 -0
- package/dist/server/app-dev-server.d.ts.map +1 -1
- package/dist/server/app-dev-server.js +305 -159
- package/dist/server/app-dev-server.js.map +1 -1
- package/dist/server/app-router-entry.d.ts.map +1 -1
- package/dist/server/app-router-entry.js +16 -3
- package/dist/server/app-router-entry.js.map +1 -1
- package/dist/server/dev-origin-check.d.ts +61 -0
- package/dist/server/dev-origin-check.d.ts.map +1 -0
- package/dist/server/dev-origin-check.js +164 -0
- package/dist/server/dev-origin-check.js.map +1 -0
- package/dist/server/dev-server.d.ts +0 -2
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +390 -372
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/image-optimization.d.ts +32 -2
- package/dist/server/image-optimization.d.ts.map +1 -1
- package/dist/server/image-optimization.js +110 -9
- package/dist/server/image-optimization.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +41 -0
- package/dist/server/middleware-codegen.d.ts.map +1 -0
- package/dist/server/middleware-codegen.js +187 -0
- package/dist/server/middleware-codegen.js.map +1 -0
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +37 -19
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/normalize-path.d.ts +22 -0
- package/dist/server/normalize-path.d.ts.map +1 -0
- package/dist/server/normalize-path.js +50 -0
- package/dist/server/normalize-path.js.map +1 -0
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +95 -26
- package/dist/server/prod-server.js.map +1 -1
- package/dist/shims/cache-runtime.d.ts +7 -0
- package/dist/shims/cache-runtime.d.ts.map +1 -1
- package/dist/shims/cache-runtime.js +19 -15
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +8 -0
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +20 -15
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts +2 -3
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +80 -9
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/head-state.d.ts +6 -1
- package/dist/shims/head-state.d.ts.map +1 -1
- package/dist/shims/head-state.js +18 -15
- package/dist/shims/head-state.js.map +1 -1
- package/dist/shims/head.d.ts.map +1 -1
- package/dist/shims/head.js +4 -1
- package/dist/shims/head.js.map +1 -1
- package/dist/shims/headers.d.ts +9 -13
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +30 -49
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/image.d.ts.map +1 -1
- package/dist/shims/image.js +11 -2
- package/dist/shims/image.js.map +1 -1
- package/dist/shims/navigation-state.d.ts +6 -1
- package/dist/shims/navigation-state.d.ts.map +1 -1
- package/dist/shims/navigation-state.js +20 -29
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.js +2 -2
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/router-state.d.ts +6 -1
- package/dist/shims/router-state.d.ts.map +1 -1
- package/dist/shims/router-state.js +16 -21
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +19 -6
- package/dist/shims/router.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import { handleApiRoute } from "./server/api-handler.js";
|
|
|
6
6
|
import { generateRscEntry, generateSsrEntry, generateBrowserEntry, } from "./server/app-dev-server.js";
|
|
7
7
|
import { loadNextConfig, resolveNextConfig, } from "./config/next-config.js";
|
|
8
8
|
import { findMiddlewareFile, runMiddleware } from "./server/middleware.js";
|
|
9
|
+
import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./server/middleware-codegen.js";
|
|
10
|
+
import { normalizePath } from "./server/normalize-path.js";
|
|
9
11
|
import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
|
|
10
|
-
import {
|
|
12
|
+
import { validateDevRequest } from "./server/dev-origin-check.js";
|
|
13
|
+
import { safeRegExp, escapeHeaderSource, isExternalUrl, proxyExternalRequest } from "./config/config-matchers.js";
|
|
11
14
|
import { scanMetadataFiles } from "./server/metadata-routes.js";
|
|
12
15
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
13
16
|
import MagicString from "magic-string";
|
|
@@ -559,103 +562,10 @@ import { NextRequest } from "next/server";`
|
|
|
559
562
|
// We inline the matching + execution logic so the prod server can call it.
|
|
560
563
|
const middlewareExportCode = middlewarePath
|
|
561
564
|
? `
|
|
562
|
-
// --- Middleware support ---
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
var patterns = [];
|
|
568
|
-
if (typeof matcher === "string") { patterns.push(matcher); }
|
|
569
|
-
else if (Array.isArray(matcher)) {
|
|
570
|
-
for (var m of matcher) {
|
|
571
|
-
if (typeof m === "string") patterns.push(m);
|
|
572
|
-
else if (m && typeof m === "object" && "source" in m) patterns.push(m.source);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return patterns.some(function(p) { return matchMiddlewarePattern(pathname, p); });
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function __isSafeRegex(pattern) {
|
|
579
|
-
var quantifierAtDepth = [];
|
|
580
|
-
var depth = 0;
|
|
581
|
-
var i = 0;
|
|
582
|
-
while (i < pattern.length) {
|
|
583
|
-
var ch = pattern[i];
|
|
584
|
-
if (ch === "\\\\") { i += 2; continue; }
|
|
585
|
-
if (ch === "[") {
|
|
586
|
-
i++;
|
|
587
|
-
while (i < pattern.length && pattern[i] !== "]") {
|
|
588
|
-
if (pattern[i] === "\\\\") i++;
|
|
589
|
-
i++;
|
|
590
|
-
}
|
|
591
|
-
i++;
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
if (ch === "(") {
|
|
595
|
-
depth++;
|
|
596
|
-
if (quantifierAtDepth.length <= depth) quantifierAtDepth.push(false);
|
|
597
|
-
else quantifierAtDepth[depth] = false;
|
|
598
|
-
i++;
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (ch === ")") {
|
|
602
|
-
var hadQ = depth > 0 && quantifierAtDepth[depth];
|
|
603
|
-
if (depth > 0) depth--;
|
|
604
|
-
var next = pattern[i + 1];
|
|
605
|
-
if (next === "+" || next === "*" || next === "{") {
|
|
606
|
-
if (hadQ) return false;
|
|
607
|
-
if (depth >= 0 && depth < quantifierAtDepth.length) quantifierAtDepth[depth] = true;
|
|
608
|
-
}
|
|
609
|
-
i++;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
if (ch === "+" || ch === "*") {
|
|
613
|
-
if (depth > 0) quantifierAtDepth[depth] = true;
|
|
614
|
-
i++;
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
if (ch === "?") {
|
|
618
|
-
var prev = i > 0 ? pattern[i - 1] : "";
|
|
619
|
-
if (prev !== "+" && prev !== "*" && prev !== "?" && prev !== "}") {
|
|
620
|
-
if (depth > 0) quantifierAtDepth[depth] = true;
|
|
621
|
-
}
|
|
622
|
-
i++;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (ch === "{") {
|
|
626
|
-
var j = i + 1;
|
|
627
|
-
while (j < pattern.length && /[\\d,]/.test(pattern[j])) j++;
|
|
628
|
-
if (j < pattern.length && pattern[j] === "}" && j > i + 1) {
|
|
629
|
-
if (depth > 0) quantifierAtDepth[depth] = true;
|
|
630
|
-
i = j + 1;
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
i++;
|
|
635
|
-
}
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
function __safeRegExp(pattern, flags) {
|
|
639
|
-
if (!__isSafeRegex(pattern)) {
|
|
640
|
-
console.warn("[vinext] Ignoring potentially unsafe regex pattern (ReDoS risk): " + pattern);
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
try { return new RegExp(pattern, flags); } catch { return null; }
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function matchMiddlewarePattern(pathname, pattern) {
|
|
647
|
-
if (pattern.includes("(") || pattern.includes("\\\\")) {
|
|
648
|
-
var re = __safeRegExp("^" + pattern + "$");
|
|
649
|
-
if (re) return re.test(pathname);
|
|
650
|
-
}
|
|
651
|
-
var regexStr = pattern
|
|
652
|
-
.replace(/\\./g, "\\\\.")
|
|
653
|
-
.replace(/\\/:([\\w]+)\\*/g, "(?:/.*)?")
|
|
654
|
-
.replace(/\\/:([\\w]+)\\+/g, "(?:/.+)")
|
|
655
|
-
.replace(/:([\\w]+)/g, "([^/]+)");
|
|
656
|
-
var re2 = __safeRegExp("^" + regexStr + "$");
|
|
657
|
-
return re2 ? re2.test(pathname) : pathname === pattern;
|
|
658
|
-
}
|
|
565
|
+
// --- Middleware support (generated from middleware-codegen.ts) ---
|
|
566
|
+
${generateNormalizePathCode("es5")}
|
|
567
|
+
${generateSafeRegExpCode("es5")}
|
|
568
|
+
${generateMiddlewareMatcherCode("es5")}
|
|
659
569
|
|
|
660
570
|
export async function runMiddleware(request) {
|
|
661
571
|
var middlewareFn = middlewareModule.default || middlewareModule.middleware;
|
|
@@ -665,7 +575,11 @@ export async function runMiddleware(request) {
|
|
|
665
575
|
var matcher = config && config.matcher;
|
|
666
576
|
var url = new URL(request.url);
|
|
667
577
|
|
|
668
|
-
|
|
578
|
+
// Normalize pathname before matching to prevent path-confusion bypasses
|
|
579
|
+
// (percent-encoding like /%61dmin, double slashes like /dashboard//settings).
|
|
580
|
+
var normalizedPathname = __normalizePath(decodeURIComponent(url.pathname));
|
|
581
|
+
|
|
582
|
+
if (!matchesMiddleware(normalizedPathname, matcher)) return { continue: true };
|
|
669
583
|
|
|
670
584
|
var nextRequest = request instanceof NextRequest ? request : new NextRequest(request);
|
|
671
585
|
var response;
|
|
@@ -715,7 +629,11 @@ import { resetSSRHead, getSSRHeadHTML } from "next/head";
|
|
|
715
629
|
import { flushPreloads } from "next/dynamic";
|
|
716
630
|
import { setSSRContext } from "next/router";
|
|
717
631
|
import { getCacheHandler } from "next/cache";
|
|
718
|
-
import {
|
|
632
|
+
import { runWithFetchCache } from "vinext/fetch-cache";
|
|
633
|
+
import { _runWithCacheState } from "next/cache";
|
|
634
|
+
import { runWithPrivateCache } from "vinext/cache-runtime";
|
|
635
|
+
import { runWithRouterState } from "vinext/router-state";
|
|
636
|
+
import { runWithHeadState } from "vinext/head-state";
|
|
719
637
|
import { safeJsonStringify } from "vinext/html";
|
|
720
638
|
import { getSSRFontLinks as _getSSRFontLinks, getSSRFontStyles as _getSSRFontStylesGoogle, getSSRFontPreloads as _getSSRFontPreloadsGoogle } from "next/font/google";
|
|
721
639
|
import { getSSRFontStyles as _getSSRFontStylesLocal, getSSRFontPreloads as _getSSRFontPreloadsLocal } from "next/font/local";
|
|
@@ -1126,7 +1044,11 @@ export async function renderPage(request, url, manifest) {
|
|
|
1126
1044
|
}
|
|
1127
1045
|
|
|
1128
1046
|
const { route, params } = match;
|
|
1129
|
-
|
|
1047
|
+
return runWithRouterState(() =>
|
|
1048
|
+
runWithHeadState(() =>
|
|
1049
|
+
_runWithCacheState(() =>
|
|
1050
|
+
runWithPrivateCache(() =>
|
|
1051
|
+
runWithFetchCache(async () => {
|
|
1130
1052
|
try {
|
|
1131
1053
|
if (typeof setSSRContext === "function") {
|
|
1132
1054
|
setSSRContext({
|
|
@@ -1193,7 +1115,7 @@ export async function renderPage(request, url, manifest) {
|
|
|
1193
1115
|
if (result && result.props) pageProps = result.props;
|
|
1194
1116
|
if (result && result.redirect) {
|
|
1195
1117
|
var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
1196
|
-
return new Response(null, { status: gsspStatus, headers: { Location: result.redirect.destination } });
|
|
1118
|
+
return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
|
|
1197
1119
|
}
|
|
1198
1120
|
if (result && result.notFound) {
|
|
1199
1121
|
return new Response("404", { status: 404 });
|
|
@@ -1252,7 +1174,7 @@ export async function renderPage(request, url, manifest) {
|
|
|
1252
1174
|
if (result && result.props) pageProps = result.props;
|
|
1253
1175
|
if (result && result.redirect) {
|
|
1254
1176
|
var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
|
|
1255
|
-
return new Response(null, { status: gspStatus, headers: { Location: result.redirect.destination } });
|
|
1177
|
+
return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
|
|
1256
1178
|
}
|
|
1257
1179
|
if (result && result.notFound) {
|
|
1258
1180
|
return new Response("404", { status: 404 });
|
|
@@ -1385,9 +1307,12 @@ export async function renderPage(request, url, manifest) {
|
|
|
1385
1307
|
} catch (e) {
|
|
1386
1308
|
console.error("[vinext] SSR error:", e);
|
|
1387
1309
|
return new Response("Internal Server Error", { status: 500 });
|
|
1388
|
-
} finally {
|
|
1389
|
-
cleanupFetchCache();
|
|
1390
1310
|
}
|
|
1311
|
+
}) // end runWithFetchCache
|
|
1312
|
+
) // end runWithPrivateCache
|
|
1313
|
+
) // end _runWithCacheState
|
|
1314
|
+
) // end runWithHeadState
|
|
1315
|
+
); // end runWithRouterState
|
|
1391
1316
|
}
|
|
1392
1317
|
|
|
1393
1318
|
export async function handleApiRoute(request, url) {
|
|
@@ -1470,6 +1395,9 @@ ${middlewareExportCode}
|
|
|
1470
1395
|
const loaderEntries = pageRoutes.map((r) => {
|
|
1471
1396
|
const absPath = r.filePath.replace(/\\/g, "/");
|
|
1472
1397
|
const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
|
|
1398
|
+
// JSON.stringify safely escapes quotes, backslashes, and special chars in
|
|
1399
|
+
// both the route pattern and the absolute file path.
|
|
1400
|
+
// lgtm[js/bad-code-sanitization]
|
|
1473
1401
|
return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
|
|
1474
1402
|
});
|
|
1475
1403
|
const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
|
|
@@ -1638,6 +1566,15 @@ hydrate();
|
|
|
1638
1566
|
// Expose image remote patterns for validation in next/image shim
|
|
1639
1567
|
defines["process.env.__VINEXT_IMAGE_REMOTE_PATTERNS"] = JSON.stringify(JSON.stringify(nextConfig.images?.remotePatterns ?? []));
|
|
1640
1568
|
defines["process.env.__VINEXT_IMAGE_DOMAINS"] = JSON.stringify(JSON.stringify(nextConfig.images?.domains ?? []));
|
|
1569
|
+
// Expose allowed image widths (union of deviceSizes + imageSizes) for
|
|
1570
|
+
// server-side validation. Matches Next.js behavior: only configured
|
|
1571
|
+
// sizes are accepted by the image optimization endpoint.
|
|
1572
|
+
{
|
|
1573
|
+
const deviceSizes = nextConfig.images?.deviceSizes ?? [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
1574
|
+
const imageSizes = nextConfig.images?.imageSizes ?? [16, 32, 48, 64, 96, 128, 256, 384];
|
|
1575
|
+
defines["process.env.__VINEXT_IMAGE_DEVICE_SIZES"] = JSON.stringify(JSON.stringify(deviceSizes));
|
|
1576
|
+
defines["process.env.__VINEXT_IMAGE_SIZES"] = JSON.stringify(JSON.stringify(imageSizes));
|
|
1577
|
+
}
|
|
1641
1578
|
// Draft mode secret — generated once at build time so the
|
|
1642
1579
|
// __prerender_bypass cookie is consistent across all server
|
|
1643
1580
|
// instances (e.g. multiple Cloudflare Workers isolates).
|
|
@@ -1812,7 +1749,15 @@ hydrate();
|
|
|
1812
1749
|
// route handlers so they can set the Allow header and run user-defined
|
|
1813
1750
|
// OPTIONS handlers. Without this, Vite's CORS middleware responds to
|
|
1814
1751
|
// OPTIONS with a 204 before the request reaches vinext's handler.
|
|
1815
|
-
|
|
1752
|
+
// Keep Vite's default restrictive origin policy by explicitly
|
|
1753
|
+
// setting it. Without the `origin` field, `preflightContinue: true`
|
|
1754
|
+
// would override Vite's default and allow any origin.
|
|
1755
|
+
server: {
|
|
1756
|
+
cors: {
|
|
1757
|
+
preflightContinue: true,
|
|
1758
|
+
origin: /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/,
|
|
1759
|
+
},
|
|
1760
|
+
},
|
|
1816
1761
|
// Externalize React packages from SSR transform — they are CJS and
|
|
1817
1762
|
// must be loaded natively by Node, not through Vite's ESM evaluator.
|
|
1818
1763
|
// Skip when targeting Cloudflare Workers (they bundle everything).
|
|
@@ -2049,6 +1994,7 @@ hydrate();
|
|
|
2049
1994
|
rewrites: nextConfig?.rewrites,
|
|
2050
1995
|
headers: nextConfig?.headers,
|
|
2051
1996
|
allowedOrigins: nextConfig?.serverActionsAllowedOrigins,
|
|
1997
|
+
allowedDevOrigins: nextConfig?.serverActionsAllowedOrigins,
|
|
2052
1998
|
});
|
|
2053
1999
|
}
|
|
2054
2000
|
if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) {
|
|
@@ -2170,16 +2116,42 @@ hydrate();
|
|
|
2170
2116
|
if (url.split("?")[0].endsWith(".rsc")) {
|
|
2171
2117
|
return next();
|
|
2172
2118
|
}
|
|
2119
|
+
// ── Cross-origin request protection ─────────────────────────
|
|
2120
|
+
// Block requests from non-localhost origins to prevent
|
|
2121
|
+
// cross-origin data exfiltration from the dev server.
|
|
2122
|
+
const blockReason = validateDevRequest({
|
|
2123
|
+
origin: req.headers.origin,
|
|
2124
|
+
host: req.headers.host,
|
|
2125
|
+
"x-forwarded-host": req.headers["x-forwarded-host"],
|
|
2126
|
+
"sec-fetch-site": req.headers["sec-fetch-site"],
|
|
2127
|
+
"sec-fetch-mode": req.headers["sec-fetch-mode"],
|
|
2128
|
+
}, nextConfig?.serverActionsAllowedOrigins);
|
|
2129
|
+
if (blockReason) {
|
|
2130
|
+
console.warn(`[vinext] Blocked dev request: ${blockReason} (${url})`);
|
|
2131
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2132
|
+
res.end("Forbidden");
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2173
2135
|
// ── Image optimization passthrough (dev mode) ─────────────
|
|
2174
2136
|
// In dev, redirect to the original asset URL so Vite serves it.
|
|
2175
2137
|
if (url.split("?")[0] === "/_vinext/image") {
|
|
2176
2138
|
const imgParams = new URLSearchParams(url.split("?")[1] ?? "");
|
|
2177
|
-
const
|
|
2139
|
+
const rawImgUrl = imgParams.get("url");
|
|
2140
|
+
// Normalize backslashes: browsers and the URL constructor treat
|
|
2141
|
+
// /\evil.com as //evil.com, bypassing the // check.
|
|
2142
|
+
const imgUrl = rawImgUrl?.replaceAll("\\", "/") ?? null;
|
|
2178
2143
|
// Allowlist: must start with "/" but not "//" — blocks absolute
|
|
2179
|
-
// URLs, protocol-relative, and exotic schemes
|
|
2144
|
+
// URLs, protocol-relative, backslash variants, and exotic schemes.
|
|
2180
2145
|
if (!imgUrl || !imgUrl.startsWith("/") || imgUrl.startsWith("//")) {
|
|
2181
2146
|
res.writeHead(400);
|
|
2182
|
-
res.end(!
|
|
2147
|
+
res.end(!rawImgUrl ? "Missing url parameter" : "Only relative URLs allowed");
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
// Validate the constructed URL's origin hasn't changed (defense in depth).
|
|
2151
|
+
const resolvedImg = new URL(imgUrl, `http://${req.headers.host || "localhost"}`);
|
|
2152
|
+
if (resolvedImg.origin !== `http://${req.headers.host || "localhost"}`) {
|
|
2153
|
+
res.writeHead(400);
|
|
2154
|
+
res.end("Only relative URLs allowed");
|
|
2183
2155
|
return;
|
|
2184
2156
|
}
|
|
2185
2157
|
res.writeHead(302, { Location: imgUrl });
|
|
@@ -2201,16 +2173,20 @@ hydrate();
|
|
|
2201
2173
|
if (pathname.includes(".") && !pathname.endsWith(".html")) {
|
|
2202
2174
|
return next();
|
|
2203
2175
|
}
|
|
2204
|
-
// Guard against protocol-relative URL open
|
|
2205
|
-
//
|
|
2206
|
-
//
|
|
2207
|
-
//
|
|
2208
|
-
|
|
2176
|
+
// Guard against protocol-relative URL open redirects.
|
|
2177
|
+
// Normalize backslashes first: browsers treat /\ as // in URL
|
|
2178
|
+
// context. Check the RAW pathname before normalizePath so the
|
|
2179
|
+
// guard fires before normalizePath collapses //.
|
|
2180
|
+
pathname = pathname.replaceAll("\\", "/");
|
|
2209
2181
|
if (pathname.startsWith("//")) {
|
|
2210
2182
|
res.writeHead(404);
|
|
2211
2183
|
res.end("404 Not Found");
|
|
2212
2184
|
return;
|
|
2213
2185
|
}
|
|
2186
|
+
// Normalize the pathname to prevent path-confusion attacks.
|
|
2187
|
+
// decodeURIComponent prevents /%61dmin bypassing /admin matchers.
|
|
2188
|
+
// normalizePath collapses // and resolves . / .. segments.
|
|
2189
|
+
pathname = normalizePath(decodeURIComponent(pathname));
|
|
2214
2190
|
// Strip basePath prefix from URL for route matching.
|
|
2215
2191
|
// All internal routing uses basePath-free paths.
|
|
2216
2192
|
//
|
|
@@ -3017,6 +2993,29 @@ function getNextPublicEnvDefines() {
|
|
|
3017
2993
|
}
|
|
3018
2994
|
return defines;
|
|
3019
2995
|
}
|
|
2996
|
+
/**
|
|
2997
|
+
* If the current position in `str` starts with a parenthesized group, consume
|
|
2998
|
+
* it and advance `re.lastIndex` past the closing `)`. Returns the group
|
|
2999
|
+
* contents or null if no group is present.
|
|
3000
|
+
*/
|
|
3001
|
+
function extractConstraint(str, re) {
|
|
3002
|
+
if (str[re.lastIndex] !== "(")
|
|
3003
|
+
return null;
|
|
3004
|
+
const start = re.lastIndex + 1;
|
|
3005
|
+
let depth = 1;
|
|
3006
|
+
let i = start;
|
|
3007
|
+
while (i < str.length && depth > 0) {
|
|
3008
|
+
if (str[i] === "(")
|
|
3009
|
+
depth++;
|
|
3010
|
+
else if (str[i] === ")")
|
|
3011
|
+
depth--;
|
|
3012
|
+
i++;
|
|
3013
|
+
}
|
|
3014
|
+
if (depth !== 0)
|
|
3015
|
+
return null;
|
|
3016
|
+
re.lastIndex = i;
|
|
3017
|
+
return str.slice(start, i - 1);
|
|
3018
|
+
}
|
|
3020
3019
|
/**
|
|
3021
3020
|
* Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
|
|
3022
3021
|
* Returns matched params or null.
|
|
@@ -3043,28 +3042,44 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
3043
3042
|
// :param* -> (.*)
|
|
3044
3043
|
// :param+ -> (.+)
|
|
3045
3044
|
const paramNames = [];
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3045
|
+
// Single-pass conversion with procedural suffix handling. The tokenizer
|
|
3046
|
+
// matches only simple, non-overlapping tokens; quantifier/constraint
|
|
3047
|
+
// suffixes after :param are consumed procedurally to avoid polynomial
|
|
3048
|
+
// backtracking in the regex engine.
|
|
3049
|
+
let regexStr = "";
|
|
3050
|
+
const tokenRe = /:(\w+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
|
|
3051
|
+
let tok;
|
|
3052
|
+
while ((tok = tokenRe.exec(pattern)) !== null) {
|
|
3053
|
+
if (tok[1] !== undefined) {
|
|
3054
|
+
const name = tok[1];
|
|
3055
|
+
const rest = pattern.slice(tokenRe.lastIndex);
|
|
3056
|
+
// Check for quantifier (* or +) with optional constraint
|
|
3057
|
+
if (rest.startsWith("*") || rest.startsWith("+")) {
|
|
3058
|
+
const quantifier = rest[0];
|
|
3059
|
+
tokenRe.lastIndex += 1;
|
|
3060
|
+
const constraint = extractConstraint(pattern, tokenRe);
|
|
3061
|
+
paramNames.push(name);
|
|
3062
|
+
if (constraint !== null) {
|
|
3063
|
+
regexStr += `(${constraint})`;
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
regexStr += quantifier === "*" ? "(.*)" : "(.+)";
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
else {
|
|
3070
|
+
// Check for inline constraint without quantifier
|
|
3071
|
+
const constraint = extractConstraint(pattern, tokenRe);
|
|
3072
|
+
paramNames.push(name);
|
|
3073
|
+
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
else if (tok[0] === ".") {
|
|
3077
|
+
regexStr += "\\.";
|
|
3078
|
+
}
|
|
3079
|
+
else {
|
|
3080
|
+
regexStr += tok[0];
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3068
3083
|
const re = safeRegExp("^" + regexStr + "$");
|
|
3069
3084
|
if (!re)
|
|
3070
3085
|
return null;
|
|
@@ -3094,7 +3109,12 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
3094
3109
|
if (isPlus && (!rest || rest === "/"))
|
|
3095
3110
|
return null;
|
|
3096
3111
|
// For :path* zero segments is fine
|
|
3097
|
-
|
|
3112
|
+
let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
|
|
3113
|
+
try {
|
|
3114
|
+
restValue = decodeURIComponent(restValue);
|
|
3115
|
+
}
|
|
3116
|
+
catch { /* malformed percent-encoding */ }
|
|
3117
|
+
return { [paramName]: restValue };
|
|
3098
3118
|
}
|
|
3099
3119
|
// Simple segment-based matching for exact patterns and :param
|
|
3100
3120
|
const parts = pattern.split("/");
|
|
@@ -3112,6 +3132,17 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
3112
3132
|
}
|
|
3113
3133
|
return params;
|
|
3114
3134
|
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Sanitize a redirect/rewrite destination by collapsing leading slashes and
|
|
3137
|
+
* backslashes to a single "/" for non-external URLs. Browsers interpret "\"
|
|
3138
|
+
* as "/" in URL contexts, so "\/evil.com" becomes "//evil.com" (protocol-relative).
|
|
3139
|
+
*/
|
|
3140
|
+
function sanitizeDestinationLocal(dest) {
|
|
3141
|
+
if (dest.startsWith("http://") || dest.startsWith("https://"))
|
|
3142
|
+
return dest;
|
|
3143
|
+
dest = dest.replace(/^[\\/]+/, "/");
|
|
3144
|
+
return dest;
|
|
3145
|
+
}
|
|
3115
3146
|
/**
|
|
3116
3147
|
* Apply redirect rules from next.config.js.
|
|
3117
3148
|
* Returns true if a redirect was applied.
|
|
@@ -3126,6 +3157,8 @@ function applyRedirects(pathname, res, redirects) {
|
|
|
3126
3157
|
dest = dest.replace(`:${key}+`, value);
|
|
3127
3158
|
dest = dest.replace(`:${key}`, value);
|
|
3128
3159
|
}
|
|
3160
|
+
// Sanitize to prevent open redirect via protocol-relative URLs
|
|
3161
|
+
dest = sanitizeDestinationLocal(dest);
|
|
3129
3162
|
res.writeHead(redirect.permanent ? 308 : 307, { Location: dest });
|
|
3130
3163
|
res.end();
|
|
3131
3164
|
return true;
|
|
@@ -3206,6 +3239,8 @@ function applyRewrites(pathname, rewrites) {
|
|
|
3206
3239
|
dest = dest.replace(`:${key}+`, value);
|
|
3207
3240
|
dest = dest.replace(`:${key}`, value);
|
|
3208
3241
|
}
|
|
3242
|
+
// Sanitize to prevent open redirect via protocol-relative URLs
|
|
3243
|
+
dest = sanitizeDestinationLocal(dest);
|
|
3209
3244
|
return dest;
|
|
3210
3245
|
}
|
|
3211
3246
|
}
|
|
@@ -3216,24 +3251,7 @@ function applyRewrites(pathname, rewrites) {
|
|
|
3216
3251
|
*/
|
|
3217
3252
|
function applyHeaders(pathname, res, headers) {
|
|
3218
3253
|
for (const rule of headers) {
|
|
3219
|
-
|
|
3220
|
-
// Strategy: extract regex groups first, process the rest, then restore groups.
|
|
3221
|
-
const groups = [];
|
|
3222
|
-
const withPlaceholders = rule.source.replace(/\(([^)]+)\)/g, (_m, inner) => {
|
|
3223
|
-
groups.push(inner);
|
|
3224
|
-
return `___GROUP_${groups.length - 1}___`;
|
|
3225
|
-
});
|
|
3226
|
-
const escaped = withPlaceholders
|
|
3227
|
-
// Escape dots and other metacharacters
|
|
3228
|
-
.replace(/\./g, "\\.")
|
|
3229
|
-
.replace(/\+/g, "\\+")
|
|
3230
|
-
.replace(/\?/g, "\\?")
|
|
3231
|
-
// Convert glob * to .*
|
|
3232
|
-
.replace(/\*/g, ".*")
|
|
3233
|
-
// Convert :param to [^/]+
|
|
3234
|
-
.replace(/:\w+/g, "[^/]+")
|
|
3235
|
-
// Restore regex groups (contents are untouched)
|
|
3236
|
-
.replace(/___GROUP_(\d+)___/g, (_m, idx) => `(${groups[Number(idx)]})`);
|
|
3254
|
+
const escaped = escapeHeaderSource(rule.source);
|
|
3237
3255
|
const sourceRegex = safeRegExp("^" + escaped + "$");
|
|
3238
3256
|
if (sourceRegex && sourceRegex.test(pathname)) {
|
|
3239
3257
|
for (const header of rule.headers) {
|