diff --git a/.changeset/fix-manifest-setter.md b/.changeset/fix-manifest-setter.md new file mode 100644 index 000000000000..3c520d534206 --- /dev/null +++ b/.changeset/fix-manifest-setter.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes `Cannot set property manifest` error in test utilities by adding a protected setter for the manifest property diff --git a/.changeset/secure-forwarded-host-validation.md b/.changeset/secure-forwarded-host-validation.md index 7e7564145a95..2bde191283c9 100644 --- a/.changeset/secure-forwarded-host-validation.md +++ b/.changeset/secure-forwarded-host-validation.md @@ -25,7 +25,9 @@ export default defineConfig({ The patterns support wildcards (`*` and `**`) for flexible hostname matching and can optionally specify protocol and port. -### Breaking change +Additionally, this fixes a bug where protocol validation was incorrectly formatted, causing valid `X-Forwarded-Host` headers to be rejected when `allowedDomains` was configured. + +__Breaking change__ Previously, `Astro.url` would reflect the value of the `X-Forwarded-Host` header. While this header is commonly used by reverse proxies like Nginx to communicate the original host, it can be sent by any client, potentially allowing malicious actors to poison caches with incorrect URLs. diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 23f1d845fac9..47592ffd5e2e 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -146,6 +146,10 @@ export class App { return this.#manifest; } + protected set manifest(value: SSRManifest) { + this.#manifest = value; + } + protected matchesAllowedDomains(forwardedHost: string, protocol?: string): boolean { return App.validateForwardedHost(forwardedHost, this.#manifest.allowedDomains, protocol); } @@ -280,7 +284,7 @@ export class App { } // Validate X-Forwarded-Host against allowedDomains if configured - if (forwardedHost && !this.matchesAllowedDomains(forwardedHost, protocol)) { + if (forwardedHost && !this.matchesAllowedDomains(forwardedHost, protocol?.replace(':', ''))) { // If not allowed, ignore the X-Forwarded-Host header forwardedHost = null; } diff --git a/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs b/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs index 2eae7dec9596..900676bbdbb5 100644 --- a/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs +++ b/packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs @@ -18,4 +18,11 @@ export default defineConfig({ } }, site: "https://example.com", + security: { + allowedDomains: [ + { hostname: 'example.pt' }, + { hostname: 'it.example.com' }, + { hostname: 'example.com' } + ] + } }) diff --git a/packages/astro/test/i18n-routing.test.js b/packages/astro/test/i18n-routing.test.js index 6f74930535ca..58b776a475f7 100644 --- a/packages/astro/test/i18n-routing.test.js +++ b/packages/astro/test/i18n-routing.test.js @@ -2140,6 +2140,13 @@ describe('i18n routing does not break assets and endpoints', () => { root: './fixtures/i18n-routing-subdomain/', output: 'server', adapter: testAdapter(), + security: { + allowedDomains: [ + { hostname: 'example.pt' }, + { hostname: 'it.example.com' }, + { hostname: 'example.com' } + ] + } }); await fixture.build(); app = await fixture.loadTestAdapterApp(); diff --git a/packages/astro/test/units/app/node.test.js b/packages/astro/test/units/app/node.test.js index c71ccb78ef17..fef0a888231c 100644 --- a/packages/astro/test/units/app/node.test.js +++ b/packages/astro/test/units/app/node.test.js @@ -59,22 +59,28 @@ describe('NodeApp', () => { describe('x-forwarded-host', () => { it('parses host from single-value x-forwarded-host header', () => { - const result = NodeApp.createRequest({ - ...mockNodeRequest, - headers: { - 'x-forwarded-host': 'www2.example.com', + const result = NodeApp.createRequest( + { + ...mockNodeRequest, + headers: { + 'x-forwarded-host': 'www2.example.com', + }, }, - }); + { allowedDomains: [{ hostname: '**.example.com' }] } + ); assert.equal(result.url, 'https://www2.example.com/'); }); it('parses host from multi-value x-forwarded-host header', () => { - const result = NodeApp.createRequest({ - ...mockNodeRequest, - headers: { - 'x-forwarded-host': 'www2.example.com,www3.example.com', + const result = NodeApp.createRequest( + { + ...mockNodeRequest, + headers: { + 'x-forwarded-host': 'www2.example.com,www3.example.com', + }, }, - }); + { allowedDomains: [{ hostname: '**.example.com' }] } + ); assert.equal(result.url, 'https://www2.example.com/'); }); @@ -171,14 +177,17 @@ describe('NodeApp', () => { }); it('prefers port from x-forwarded-host', () => { - const result = NodeApp.createRequest({ - ...mockNodeRequest, - headers: { - host: 'example.com:443', - 'x-forwarded-host': 'example.com:3000', - 'x-forwarded-port': '443', + const result = NodeApp.createRequest( + { + ...mockNodeRequest, + headers: { + host: 'example.com:443', + 'x-forwarded-host': 'example.com:3000', + 'x-forwarded-port': '443', + }, }, - }); + { allowedDomains: [{ hostname: 'example.com' }] } + ); assert.equal(result.url, 'https://example.com:3000/'); }); });