CVE Patch Review

GHSA-8MV7-9C27-98VC: Astro/Hono CSRF Bypass Fixed at Dispatch Sinks

GHSA-8MV7-9C27-98VC · Updated 2026-07-21 Root-cause

Summary

The patch addresses a CSRF bypass caused by relying on middleware ordering in Astro's composable Hono pipeline. The fix extracts origin-check logic into a shared helper and enforces it directly in both Astro Actions and on-demand page/endpoint dispatch paths, preventing cross-origin POST handling even when middleware() is mounted later or omitted.

Analysis

Vulnerability

GHSA-8MV7-9C27-98VC describes a CSRF bypass in Astro's composable astro/hono pipeline. The vulnerable design coupled security.checkOrigin to the global origin-check middleware, assuming that middleware would always execute before request dispatch. In practice, Hono composition allowed actions() to run before middleware(), and pages() could be mounted without middleware() at all. Under those compositions, state-changing POST requests reached Astro Actions or on-demand endpoints without validating the Origin header.

The root issue was architectural: CSRF enforcement lived in one pipeline stage, while request handling could legally bypass that stage. The vulnerable middleware logic rejected cross-site non-safe requests with form-like content types, or requests lacking a content type, but only if the middleware actually ran. The advisory summary and patch notes explicitly state that the check could be skipped depending on middleware ordering or omission in the composable pipeline, creating blind CSRF exposure on mutating endpoints. See the advisory and patch discussion at GitHub Advisory and PR #17250.

export function createOriginCheckMiddleware(): MiddlewareHandler {
	return defineMiddleware((context, next) => {
		const { request, url, isPrerendered } = context;
		// Prerendered pages should be excluded
		if (isPrerendered) {
			return next();
		}
		// Safe methods don't require origin check
		if (SAFE_METHODS.includes(request.method)) {
			return next();
		}
		const isSameOrigin = request.headers.get('origin') === url.origin;

		const hasContentType = request.headers.has('content-type');
		if (hasContentType) {
			const formLikeHeader = hasFormLikeHeader(request.headers.get('content-type'));
			if (formLikeHeader && !isSameOrigin) {
				return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
					status: 403,
				});
			}
		} else {
			if (!isSameOrigin) {
				return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
					status: 403,
				});
			}
		}

		return next();
	});
}

This snippet, from the vulnerable implementation summarized in the supplied sources, shows that the protection existed only as middleware logic rather than as an invariant at the actual execution sinks.

Patch

The patch moves the origin-check logic into a dedicated shared module, packages/astro/src/core/app/origin-check.ts, and exposes two reusable primitives: isForbiddenCrossOriginRequest() and createCrossOriginForbiddenResponse(). The existing middleware is then rewritten as a thin wrapper around the shared predicate, and the same predicate is invoked directly inside both the Astro Actions handler and the on-demand pages/endpoint handler. This is documented in the changeset and implemented in commit 0b30b35f864310bee8485c952d1877e82e2b9b1a.

Concretely, the patch adds pre-dispatch checks in two previously exposed sinks:

  • packages/astro/src/actions/handler.ts: blocks cross-origin action requests before the action handler executes when manifest.checkOrigin is enabled.
  • packages/astro/src/core/pages/handler.ts: blocks cross-origin on-demand endpoint/page requests before handle() executes, even if middleware() was never mounted.

The base pipeline import is also updated to source the middleware from the new shared module, ensuring one canonical implementation. The tests added in PR #17250 cover both problematic compositions: actions() mounted before middleware(), and pages() mounted without middleware().

export function isForbiddenCrossOriginRequest(
	request: Request,
	url: URL,
	isPrerendered: boolean,
): boolean {
	// Prerendered pages should be excluded
	if (isPrerendered) {
		return false;
	}
	// Safe methods don't require origin check
	if (SAFE_METHODS.includes(request.method)) {
		return false;
	}
	const isSameOrigin = request.headers.get('origin') === url.origin;

	const hasContentType = request.headers.has('content-type');
	if (hasContentType) {
		const formLikeHeader = hasFormLikeHeader(request.headers.get('content-type'));
		return formLikeHeader && !isSameOrigin;
	}
	return !isSameOrigin;
}

This refactoring is important because it converts CSRF validation from an ordering-sensitive middleware concern into a sink-enforced policy that remains active regardless of pipeline composition. Source references: PR #17250, fix commit.

Review

Pros

  • The patch addresses the actual trust-boundary failure: dispatch paths no longer assume middleware ordering for security-critical validation.
  • Shared logic in origin-check.ts reduces drift between middleware and direct dispatch enforcement.
  • Coverage is materially improved with regression tests for both known bypass modes: reordered actions()/middleware() and omitted middleware() for pages().
  • The fix preserves the existing manifest.checkOrigin feature flag semantics rather than silently changing product behavior.
  • The response construction is centralized, which improves consistency of rejection behavior across sinks.

Cons

  • The same policy is now enforced in multiple call sites, even though the predicate is shared. Future dispatch surfaces could still regress if they fail to call the helper.
  • The patch is scoped to Astro Actions and on-demand endpoint/page dispatch. It does not establish a framework-wide mechanism that automatically wraps every mutating sink.
  • The underlying CSRF model still relies on strict Origin equality and form-like content-type heuristics. That is consistent with the prior design, but the patch does not broaden or harden the policy beyond fixing the bypass.
  • No evidence in the supplied sources shows additional negative tests for other composable primitives or future extension points beyond the two identified sinks.

Verdict

Root-cause.

This is a root-cause fix for the reported vulnerability because it removes the unsafe assumption that middleware execution is guaranteed before sensitive request handling. By enforcing the same origin-check predicate directly at the Astro Actions and pages/endpoint dispatch sinks, the patch closes the structural gap that enabled the bypass in valid Hono compositions. The added tests are well aligned with the exploit conditions described in the advisory and materially increase confidence that the specific regression will not recur in those paths.

That said, the broader engineering lesson remains: in composable pipelines, security controls should be attached to invariants or sinks, not to optional or reorderable middleware stages. The current patch does that for the known affected handlers, and based on the supplied sources, it is technically sound and appropriately targeted. References: GitHub Advisory, PR #17250, commit 0b30b35, CVE Reports summary.

Sources