diff --git a/.changeset/cloudflare-image-binding-no-redirect-follow.md b/.changeset/cloudflare-image-binding-no-redirect-follow.md new file mode 100644 index 000000000000..8266b7f0cd59 --- /dev/null +++ b/.changeset/cloudflare-image-binding-no-redirect-follow.md @@ -0,0 +1,5 @@ +--- +'@astrojs/cloudflare': patch +--- + +Uses `redirect: 'manual'` for remote image fetches in the Cloudflare binding image transform, consistent with all other image fetch paths diff --git a/packages/integrations/cloudflare/src/utils/image-binding-transform.ts b/packages/integrations/cloudflare/src/utils/image-binding-transform.ts index d1d0c2db555e..d4f3311afbea 100644 --- a/packages/integrations/cloudflare/src/utils/image-binding-transform.ts +++ b/packages/integrations/cloudflare/src/utils/image-binding-transform.ts @@ -25,7 +25,14 @@ export async function transform( } const imageSrc = new URL(href, url.origin); - const content = await (isRemotePath(href) ? fetch(imageSrc) : assets.fetch(imageSrc)); + const content = await (isRemotePath(href) + ? fetch(imageSrc, { redirect: 'manual' }) + : assets.fetch(imageSrc)); + + if (content.status >= 300 && content.status < 400) { + return new Response('Not Found', { status: 404 }); + } + if (!content.body) { return new Response(null, { status: 404 }); } diff --git a/packages/integrations/cloudflare/test/binding-image-service.test.js b/packages/integrations/cloudflare/test/binding-image-service.test.js index a5504620471d..953e3cf0b1e2 100644 --- a/packages/integrations/cloudflare/test/binding-image-service.test.js +++ b/packages/integrations/cloudflare/test/binding-image-service.test.js @@ -1,12 +1,28 @@ import * as assert from 'node:assert/strict'; +import { createServer } from 'node:http'; import { after, before, describe, it } from 'node:test'; import { loadFixture } from './_test-utils.js'; describe('BindingImageService', () => { let fixture; let previewServer; + let redirectServer; + let redirectServerPort; before(async () => { + // Start a local HTTP server that always responds with a 302 redirect. + // Used to test that the image transform endpoint does not follow redirects. + redirectServer = createServer((_req, res) => { + res.writeHead(302, { Location: 'http://example.com/secret' }); + res.end(); + }); + await new Promise((resolve) => { + redirectServer.listen(0, () => { + redirectServerPort = redirectServer.address().port; + resolve(); + }); + }); + fixture = await loadFixture({ root: './fixtures/binding-image-service/', }); @@ -16,6 +32,7 @@ describe('BindingImageService', () => { after(async () => { await previewServer.stop(); + await new Promise((resolve) => redirectServer.close(resolve)); }); it('returns 403 for missing href parameter', async () => { @@ -52,4 +69,10 @@ describe('BindingImageService', () => { assert.equal(res.status, 200); assert.equal(res.headers.get('content-type'), 'image/avif'); }); + + it('does not follow redirects for remote images', async () => { + const href = `http://localhost:${redirectServerPort}/image.jpg`; + const res = await fixture.fetch(`/_image?href=${encodeURIComponent(href)}&f=webp`); + assert.equal(res.status, 404); + }); }); diff --git a/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs index 398d7db71019..421833f961d2 100644 --- a/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs @@ -5,5 +5,8 @@ export default defineConfig({ adapter: cloudflare({ imageService: 'cloudflare-binding', }), + image: { + domains: ['localhost'], + }, output: 'server', });