astro 6.4.7 → 6.4.8

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.
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.4.7";
3
+ version = "6.4.8";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -197,7 +197,7 @@ ${contentConfig.error.message}`
197
197
  logger.info("Content config changed");
198
198
  shouldClear = true;
199
199
  }
200
- if (previousAstroVersion && previousAstroVersion !== "6.4.7") {
200
+ if (previousAstroVersion && previousAstroVersion !== "6.4.8") {
201
201
  logger.info("Astro version changed");
202
202
  shouldClear = true;
203
203
  }
@@ -205,8 +205,8 @@ ${contentConfig.error.message}`
205
205
  logger.info("Clearing content store");
206
206
  this.#store.clearAll();
207
207
  }
208
- if ("6.4.7") {
209
- this.#store.metaStore().set("astro-version", "6.4.7");
208
+ if ("6.4.8") {
209
+ this.#store.metaStore().set("astro-version", "6.4.8");
210
210
  }
211
211
  if (currentConfigDigest) {
212
212
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "6.4.7";
1
+ const ASTRO_VERSION = "6.4.8";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
4
4
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
@@ -37,7 +37,7 @@ async function dev(inlineConfig) {
37
37
  await telemetry.record([]);
38
38
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
39
39
  const logger = restart.container.logger;
40
- const currentVersion = "6.4.7";
40
+ const currentVersion = "6.4.8";
41
41
  const isPrerelease = currentVersion.includes("-");
42
42
  if (!isPrerelease) {
43
43
  try {
@@ -134,6 +134,12 @@ export declare class FetchState implements AstroFetchState {
134
134
  status: number;
135
135
  /** Whether user middleware should be skipped for this request. */
136
136
  skipMiddleware: boolean;
137
+ /**
138
+ * Set to `true` when the request path was encoded too many times to fully
139
+ * decode (see {@link validateAndDecodePathname}). These requests are
140
+ * rejected with a `400` before middleware or routing run.
141
+ */
142
+ invalidEncoding: boolean;
137
143
  /** A flag that tells the render content if the rewriting was triggered. */
138
144
  isRewriting: boolean;
139
145
  /** A safety net in case of loops (rewrite counter). */
@@ -29,7 +29,7 @@ import { getParams, getProps } from "../render/index.js";
29
29
  import { Rewrites } from "../rewrites/handler.js";
30
30
  import { isRoute404or500, isRouteServerIsland } from "../routing/match.js";
31
31
  import { normalizeUrl } from "../util/normalized-url.js";
32
- import { validateAndDecodePathname } from "../util/pathname.js";
32
+ import { MultiLevelEncodingError, validateAndDecodePathname } from "../util/pathname.js";
33
33
  import { getOriginPathname, setOriginPathname } from "../routing/rewrite.js";
34
34
  import { computePathnameFromDomain } from "../i18n/domain.js";
35
35
  import { getCustom404Route, routeHasHtmlExtension } from "../routing/helpers.js";
@@ -88,6 +88,12 @@ class FetchState {
88
88
  status = 200;
89
89
  /** Whether user middleware should be skipped for this request. */
90
90
  skipMiddleware = false;
91
+ /**
92
+ * Set to `true` when the request path was encoded too many times to fully
93
+ * decode (see {@link validateAndDecodePathname}). These requests are
94
+ * rejected with a `400` before middleware or routing run.
95
+ */
96
+ invalidEncoding = false;
91
97
  /** A flag that tells the render content if the rewriting was triggered. */
92
98
  isRewriting = false;
93
99
  /** A safety net in case of loops (rewrite counter). */
@@ -681,6 +687,10 @@ class FetchState {
681
687
  try {
682
688
  return validateAndDecodePathname(pathname);
683
689
  } catch (e) {
690
+ if (e instanceof MultiLevelEncodingError) {
691
+ this.invalidEncoding = true;
692
+ return pathname;
693
+ }
684
694
  this.pipeline.logger.error(null, e.toString());
685
695
  return pathname;
686
696
  }
@@ -47,7 +47,7 @@ class I18n {
47
47
  if (typeHeader !== "page" && typeHeader !== "fallback") {
48
48
  return response;
49
49
  }
50
- const url = new URL(state.request.url);
50
+ const url = state.url;
51
51
  const currentLocale = state.computeCurrentLocale();
52
52
  const isPrerendered = state.routeData.prerender;
53
53
  const routerContext = {
@@ -276,7 +276,7 @@ function printHelp({
276
276
  message.push(
277
277
  linebreak(),
278
278
  ` ${bgGreen(black(` ${commandName} `))} ${green(
279
- `v${"6.4.7"}`
279
+ `v${"6.4.8"}`
280
280
  )} ${headline}`
281
281
  );
282
282
  }
@@ -65,6 +65,9 @@ class AstroHandler {
65
65
  }
66
66
  async handle(state) {
67
67
  state.pipeline.usedFeatures |= ALL_PIPELINE_FEATURES;
68
+ if (state.invalidEncoding) {
69
+ return new Response(null, { status: 400, statusText: "Bad Request" });
70
+ }
68
71
  const trailingSlashRedirect = this.#trailingSlashHandler.handle(state);
69
72
  if (trailingSlashRedirect) {
70
73
  return trailingSlashRedirect;
@@ -10,6 +10,7 @@ import {
10
10
  trimSlashes
11
11
  } from "../path.js";
12
12
  import { createRequest } from "../request.js";
13
+ import { validateAndDecodePathname } from "../util/pathname.js";
13
14
  import { DEFAULT_404_ROUTE } from "./internal/astro-designed-error-pages.js";
14
15
  import { isRoute404, isRoute500 } from "./internal/route-errors.js";
15
16
  function findRouteToRewrite({
@@ -36,7 +37,7 @@ function findRouteToRewrite({
36
37
  buildFormat
37
38
  );
38
39
  newUrl.pathname = resolvedUrlPathname;
39
- const decodedPathname = decodeURI(pathname);
40
+ const decodedPathname = validateAndDecodePathname(pathname);
40
41
  if (isRoute404(decodedPathname)) {
41
42
  const errorRoute = routes.find((route) => route.route === "/404");
42
43
  if (errorRoute) {
@@ -1,25 +1,26 @@
1
1
  /**
2
- * Error thrown when multi-level URL encoding is detected in a pathname.
3
- * This is a distinct error type so callers can handle it specifically
4
- * (e.g., returning a 400 response) rather than falling back to partial decoding.
5
- *
6
- * @deprecated No longer thrown internally — multi-level encoding is now
7
- * decoded iteratively instead of rejected. Kept for backwards compatibility
8
- * in case third-party code references the class.
2
+ * Thrown when a URL path is encoded so many times that we give up decoding it
3
+ * (see {@link validateAndDecodePathname}). When this happens we reject the
4
+ * request with a `400` instead of guessing the path. If we let a half-decoded
5
+ * path through, your middleware might check one path while Astro routes to a
6
+ * different one.
9
7
  */
10
8
  export declare class MultiLevelEncodingError extends Error {
11
9
  constructor();
12
10
  }
13
11
  /**
14
- * Decodes a pathname iteratively until stable, collapsing all levels of
15
- * percent-encoding into a single canonical form. This prevents
16
- * double/triple encoding from bypassing middleware authorization checks
17
- * (CVE-2025-66202)instead of rejecting multi-level encoding, we
18
- * fully resolve it so middleware always sees the true decoded path.
12
+ * Decodes a URL path over and over until it stops changing, so a path that was
13
+ * encoded several times ends up as a single, final path. This stops someone
14
+ * from sneaking a path like `/admin` past middleware by encoding it multiple
15
+ * timesmiddleware always sees the real, decoded path.
19
16
  *
20
- * @param pathname - The pathname to decode
21
- * @returns The fully decoded pathname
22
- * @throws Error if the pathname contains invalid URL encoding that
23
- * cannot be decoded at all (e.g., a bare `%` not followed by hex digits)
17
+ * @param pathname - The path to decode
18
+ * @returns The final, fully decoded path
19
+ * @throws Error if the path has broken encoding that can't be decoded at all
20
+ * (for example a lone `%` that isn't followed by two hex digits)
21
+ * @throws MultiLevelEncodingError if the path is still changing after
22
+ * {@link MAX_DECODE_ITERATIONS} tries (it was encoded too many times).
23
+ * Handing back a half-decoded path here would bring back the security hole
24
+ * this function exists to close.
24
25
  */
25
26
  export declare function validateAndDecodePathname(pathname: string): string;
@@ -1,9 +1,10 @@
1
1
  class MultiLevelEncodingError extends Error {
2
2
  constructor() {
3
- super("Multi-level URL encoding is not allowed");
3
+ super("URL encoding depth exceeded the maximum number of decode iterations");
4
4
  this.name = "MultiLevelEncodingError";
5
5
  }
6
6
  }
7
+ const MAX_DECODE_ITERATIONS = 10;
7
8
  function validateAndDecodePathname(pathname) {
8
9
  let decoded;
9
10
  try {
@@ -12,7 +13,10 @@ function validateAndDecodePathname(pathname) {
12
13
  throw new Error("Invalid URL encoding");
13
14
  }
14
15
  let iterations = 0;
15
- while (decoded !== pathname && iterations < 10) {
16
+ while (decoded !== pathname) {
17
+ if (iterations >= MAX_DECODE_ITERATIONS) {
18
+ throw new MultiLevelEncodingError();
19
+ }
16
20
  pathname = decoded;
17
21
  try {
18
22
  decoded = decodeURI(pathname);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "6.4.7",
3
+ "version": "6.4.8",
4
4
  "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
5
5
  "type": "module",
6
6
  "author": "withastro",
@@ -166,8 +166,8 @@
166
166
  "yargs-parser": "^22.0.0",
167
167
  "zod": "^4.3.6",
168
168
  "@astrojs/internal-helpers": "0.10.0",
169
- "@astrojs/markdown-remark": "7.2.0",
170
- "@astrojs/telemetry": "3.3.2"
169
+ "@astrojs/telemetry": "3.3.2",
170
+ "@astrojs/markdown-remark": "7.2.0"
171
171
  },
172
172
  "optionalDependencies": {
173
173
  "sharp": "^0.34.0"