vinext 0.0.10 → 0.0.12

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.
Files changed (49) hide show
  1. package/dist/cli.js +4 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config/config-matchers.d.ts +11 -2
  4. package/dist/config/config-matchers.d.ts.map +1 -1
  5. package/dist/config/config-matchers.js +167 -45
  6. package/dist/config/config-matchers.js.map +1 -1
  7. package/dist/deploy.d.ts.map +1 -1
  8. package/dist/deploy.js +6 -4
  9. package/dist/deploy.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +94 -51
  12. package/dist/index.js.map +1 -1
  13. package/dist/init.d.ts.map +1 -1
  14. package/dist/init.js +3 -2
  15. package/dist/init.js.map +1 -1
  16. package/dist/server/app-dev-server.d.ts.map +1 -1
  17. package/dist/server/app-dev-server.js +34 -9
  18. package/dist/server/app-dev-server.js.map +1 -1
  19. package/dist/server/app-router-entry.d.ts.map +1 -1
  20. package/dist/server/app-router-entry.js +8 -1
  21. package/dist/server/app-router-entry.js.map +1 -1
  22. package/dist/server/dev-server.d.ts.map +1 -1
  23. package/dist/server/dev-server.js +14 -2
  24. package/dist/server/dev-server.js.map +1 -1
  25. package/dist/server/middleware-codegen.d.ts.map +1 -1
  26. package/dist/server/middleware-codegen.js +13 -7
  27. package/dist/server/middleware-codegen.js.map +1 -1
  28. package/dist/server/middleware.d.ts.map +1 -1
  29. package/dist/server/middleware.js +34 -13
  30. package/dist/server/middleware.js.map +1 -1
  31. package/dist/server/prod-server.d.ts.map +1 -1
  32. package/dist/server/prod-server.js +26 -3
  33. package/dist/server/prod-server.js.map +1 -1
  34. package/dist/shims/fetch-cache.d.ts.map +1 -1
  35. package/dist/shims/fetch-cache.js +153 -55
  36. package/dist/shims/fetch-cache.js.map +1 -1
  37. package/dist/shims/head.d.ts.map +1 -1
  38. package/dist/shims/head.js +4 -1
  39. package/dist/shims/head.js.map +1 -1
  40. package/dist/shims/headers.d.ts.map +1 -1
  41. package/dist/shims/headers.js +4 -2
  42. package/dist/shims/headers.js.map +1 -1
  43. package/dist/shims/navigation.js +2 -2
  44. package/dist/shims/navigation.js.map +1 -1
  45. package/dist/shims/router.d.ts +1 -1
  46. package/dist/shims/router.d.ts.map +1 -1
  47. package/dist/shims/router.js +2 -2
  48. package/dist/shims/router.js.map +1 -1
  49. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormaliz
10
10
  import { normalizePath } from "./server/normalize-path.js";
11
11
  import { findInstrumentationFile, runInstrumentation } from "./server/instrumentation.js";
12
12
  import { validateDevRequest } from "./server/dev-origin-check.js";
13
- import { safeRegExp, isExternalUrl, proxyExternalRequest } from "./config/config-matchers.js";
13
+ import { safeRegExp, escapeHeaderSource, isExternalUrl, proxyExternalRequest } from "./config/config-matchers.js";
14
14
  import { scanMetadataFiles } from "./server/metadata-routes.js";
15
15
  import tsconfigPaths from "vite-tsconfig-paths";
16
16
  import MagicString from "magic-string";
@@ -577,7 +577,11 @@ export async function runMiddleware(request) {
577
577
 
578
578
  // Normalize pathname before matching to prevent path-confusion bypasses
579
579
  // (percent-encoding like /%61dmin, double slashes like /dashboard//settings).
580
- var normalizedPathname = __normalizePath(decodeURIComponent(url.pathname));
580
+ var decodedPathname;
581
+ try { decodedPathname = decodeURIComponent(url.pathname); } catch (e) {
582
+ return { continue: false, response: new Response("Bad Request", { status: 400 }) };
583
+ }
584
+ var normalizedPathname = __normalizePath(decodedPathname);
581
585
 
582
586
  if (!matchesMiddleware(normalizedPathname, matcher)) return { continue: true };
583
587
 
@@ -896,7 +900,8 @@ function parseCookieLocaleFromHeader(cookieHeader) {
896
900
  if (!i18nConfig || !cookieHeader) return null;
897
901
  const match = cookieHeader.match(/(?:^|;\\s*)NEXT_LOCALE=([^;]*)/);
898
902
  if (!match) return null;
899
- const value = decodeURIComponent(match[1].trim());
903
+ var value;
904
+ try { value = decodeURIComponent(match[1].trim()); } catch (e) { return null; }
900
905
  if (i18nConfig.locales.indexOf(value) !== -1) return value;
901
906
  return null;
902
907
  }
@@ -1115,7 +1120,7 @@ export async function renderPage(request, url, manifest) {
1115
1120
  if (result && result.props) pageProps = result.props;
1116
1121
  if (result && result.redirect) {
1117
1122
  var gsspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1118
- return new Response(null, { status: gsspStatus, headers: { Location: result.redirect.destination } });
1123
+ return new Response(null, { status: gsspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
1119
1124
  }
1120
1125
  if (result && result.notFound) {
1121
1126
  return new Response("404", { status: 404 });
@@ -1174,7 +1179,7 @@ export async function renderPage(request, url, manifest) {
1174
1179
  if (result && result.props) pageProps = result.props;
1175
1180
  if (result && result.redirect) {
1176
1181
  var gspStatus = result.redirect.statusCode != null ? result.redirect.statusCode : (result.redirect.permanent ? 308 : 307);
1177
- return new Response(null, { status: gspStatus, headers: { Location: result.redirect.destination } });
1182
+ return new Response(null, { status: gspStatus, headers: { Location: sanitizeDestinationLocal(result.redirect.destination) } });
1178
1183
  }
1179
1184
  if (result && result.notFound) {
1180
1185
  return new Response("404", { status: 404 });
@@ -1395,6 +1400,9 @@ ${middlewareExportCode}
1395
1400
  const loaderEntries = pageRoutes.map((r) => {
1396
1401
  const absPath = r.filePath.replace(/\\/g, "/");
1397
1402
  const nextFormatPattern = pagesPatternToNextFormat(r.pattern);
1403
+ // JSON.stringify safely escapes quotes, backslashes, and special chars in
1404
+ // both the route pattern and the absolute file path.
1405
+ // lgtm[js/bad-code-sanitization]
1398
1406
  return ` ${JSON.stringify(nextFormatPattern)}: () => import(${JSON.stringify(absPath)})`;
1399
1407
  });
1400
1408
  const appFileBase = path.join(pagesDir, "_app").replace(/\\/g, "/");
@@ -2183,7 +2191,15 @@ hydrate();
2183
2191
  // Normalize the pathname to prevent path-confusion attacks.
2184
2192
  // decodeURIComponent prevents /%61dmin bypassing /admin matchers.
2185
2193
  // normalizePath collapses // and resolves . / .. segments.
2186
- pathname = normalizePath(decodeURIComponent(pathname));
2194
+ try {
2195
+ pathname = normalizePath(decodeURIComponent(pathname));
2196
+ }
2197
+ catch {
2198
+ // Malformed percent-encoding (e.g. /%E0%A4%A) — return 400 instead of crashing.
2199
+ res.writeHead(400);
2200
+ res.end("Bad Request");
2201
+ return;
2202
+ }
2187
2203
  // Strip basePath prefix from URL for route matching.
2188
2204
  // All internal routing uses basePath-free paths.
2189
2205
  //
@@ -2990,6 +3006,29 @@ function getNextPublicEnvDefines() {
2990
3006
  }
2991
3007
  return defines;
2992
3008
  }
3009
+ /**
3010
+ * If the current position in `str` starts with a parenthesized group, consume
3011
+ * it and advance `re.lastIndex` past the closing `)`. Returns the group
3012
+ * contents or null if no group is present.
3013
+ */
3014
+ function extractConstraint(str, re) {
3015
+ if (str[re.lastIndex] !== "(")
3016
+ return null;
3017
+ const start = re.lastIndex + 1;
3018
+ let depth = 1;
3019
+ let i = start;
3020
+ while (i < str.length && depth > 0) {
3021
+ if (str[i] === "(")
3022
+ depth++;
3023
+ else if (str[i] === ")")
3024
+ depth--;
3025
+ i++;
3026
+ }
3027
+ if (depth !== 0)
3028
+ return null;
3029
+ re.lastIndex = i;
3030
+ return str.slice(start, i - 1);
3031
+ }
2993
3032
  /**
2994
3033
  * Match a Next.js route pattern (e.g. "/blog/:slug", "/docs/:path*") against a pathname.
2995
3034
  * Returns matched params or null.
@@ -3016,28 +3055,44 @@ export function matchConfigPattern(pathname, pattern) {
3016
3055
  // :param* -> (.*)
3017
3056
  // :param+ -> (.+)
3018
3057
  const paramNames = [];
3019
- const regexStr = pattern
3020
- .replace(/\./g, "\\.")
3021
- // :param* with optional constraint
3022
- .replace(/:(\w+)\*(?:\(([^)]+)\))?/g, (_m, name, constraint) => {
3023
- paramNames.push(name);
3024
- return constraint ? `(${constraint})` : "(.*)";
3025
- })
3026
- // :param+ with optional constraint
3027
- .replace(/:(\w+)\+(?:\(([^)]+)\))?/g, (_m, name, constraint) => {
3028
- paramNames.push(name);
3029
- return constraint ? `(${constraint})` : "(.+)";
3030
- })
3031
- // :param(constraint) named param with inline regex constraint
3032
- .replace(/:(\w+)\(([^)]+)\)/g, (_m, name, constraint) => {
3033
- paramNames.push(name);
3034
- return `(${constraint})`;
3035
- })
3036
- // :param plain named param
3037
- .replace(/:(\w+)/g, (_m, name) => {
3038
- paramNames.push(name);
3039
- return "([^/]+)";
3040
- });
3058
+ // Single-pass conversion with procedural suffix handling. The tokenizer
3059
+ // matches only simple, non-overlapping tokens; quantifier/constraint
3060
+ // suffixes after :param are consumed procedurally to avoid polynomial
3061
+ // backtracking in the regex engine.
3062
+ let regexStr = "";
3063
+ const tokenRe = /:(\w+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
3064
+ let tok;
3065
+ while ((tok = tokenRe.exec(pattern)) !== null) {
3066
+ if (tok[1] !== undefined) {
3067
+ const name = tok[1];
3068
+ const rest = pattern.slice(tokenRe.lastIndex);
3069
+ // Check for quantifier (* or +) with optional constraint
3070
+ if (rest.startsWith("*") || rest.startsWith("+")) {
3071
+ const quantifier = rest[0];
3072
+ tokenRe.lastIndex += 1;
3073
+ const constraint = extractConstraint(pattern, tokenRe);
3074
+ paramNames.push(name);
3075
+ if (constraint !== null) {
3076
+ regexStr += `(${constraint})`;
3077
+ }
3078
+ else {
3079
+ regexStr += quantifier === "*" ? "(.*)" : "(.+)";
3080
+ }
3081
+ }
3082
+ else {
3083
+ // Check for inline constraint without quantifier
3084
+ const constraint = extractConstraint(pattern, tokenRe);
3085
+ paramNames.push(name);
3086
+ regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
3087
+ }
3088
+ }
3089
+ else if (tok[0] === ".") {
3090
+ regexStr += "\\.";
3091
+ }
3092
+ else {
3093
+ regexStr += tok[0];
3094
+ }
3095
+ }
3041
3096
  const re = safeRegExp("^" + regexStr + "$");
3042
3097
  if (!re)
3043
3098
  return null;
@@ -3067,7 +3122,12 @@ export function matchConfigPattern(pathname, pattern) {
3067
3122
  if (isPlus && (!rest || rest === "/"))
3068
3123
  return null;
3069
3124
  // For :path* zero segments is fine
3070
- return { [paramName]: rest.startsWith("/") ? rest.slice(1) : rest };
3125
+ let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
3126
+ try {
3127
+ restValue = decodeURIComponent(restValue);
3128
+ }
3129
+ catch { /* malformed percent-encoding */ }
3130
+ return { [paramName]: restValue };
3071
3131
  }
3072
3132
  // Simple segment-based matching for exact patterns and :param
3073
3133
  const parts = pattern.split("/");
@@ -3086,14 +3146,14 @@ export function matchConfigPattern(pathname, pattern) {
3086
3146
  return params;
3087
3147
  }
3088
3148
  /**
3089
- * Sanitize a redirect/rewrite destination by collapsing leading // to /
3090
- * for non-external URLs, preventing unintended protocol-relative redirects.
3149
+ * Sanitize a redirect/rewrite destination by collapsing leading slashes and
3150
+ * backslashes to a single "/" for non-external URLs. Browsers interpret "\"
3151
+ * as "/" in URL contexts, so "\/evil.com" becomes "//evil.com" (protocol-relative).
3091
3152
  */
3092
3153
  function sanitizeDestinationLocal(dest) {
3093
3154
  if (dest.startsWith("http://") || dest.startsWith("https://"))
3094
3155
  return dest;
3095
- if (dest.startsWith("//"))
3096
- dest = dest.replace(/^\/\/+/, "/");
3156
+ dest = dest.replace(/^[\\/]+/, "/");
3097
3157
  return dest;
3098
3158
  }
3099
3159
  /**
@@ -3204,24 +3264,7 @@ function applyRewrites(pathname, rewrites) {
3204
3264
  */
3205
3265
  function applyHeaders(pathname, res, headers) {
3206
3266
  for (const rule of headers) {
3207
- // Escape regex metacharacters in the source, then convert Next.js patterns.
3208
- // Strategy: extract regex groups first, process the rest, then restore groups.
3209
- const groups = [];
3210
- const withPlaceholders = rule.source.replace(/\(([^)]+)\)/g, (_m, inner) => {
3211
- groups.push(inner);
3212
- return `___GROUP_${groups.length - 1}___`;
3213
- });
3214
- const escaped = withPlaceholders
3215
- // Escape dots and other metacharacters
3216
- .replace(/\./g, "\\.")
3217
- .replace(/\+/g, "\\+")
3218
- .replace(/\?/g, "\\?")
3219
- // Convert glob * to .*
3220
- .replace(/\*/g, ".*")
3221
- // Convert :param to [^/]+
3222
- .replace(/:\w+/g, "[^/]+")
3223
- // Restore regex groups (contents are untouched)
3224
- .replace(/___GROUP_(\d+)___/g, (_m, idx) => `(${groups[Number(idx)]})`);
3267
+ const escaped = escapeHeaderSource(rule.source);
3225
3268
  const sourceRegex = safeRegExp("^" + escaped + "$");
3226
3269
  if (sourceRegex && sourceRegex.test(pathname)) {
3227
3270
  for (const header of rule.headers) {