VYPR
Moderate severityNVD Advisory· Published Aug 19, 2025· Updated Aug 19, 2025

Unauthorized third-party images in Astro’s _image endpoint

CVE-2025-55303

Description

Astro is a web framework for content-driven websites. In versions of astro before 5.13.2 and 4.16.18, the image optimization endpoint in projects deployed with on-demand rendering allows images from unauthorized third-party domains to be served. On-demand rendered sites built with Astro include an /_image endpoint which returns optimized versions of images. A bug in impacted versions of astro allows an attacker to bypass the third-party domain restrictions by using a protocol-relative URL as the image source, e.g. /_image?href=//example.com/image.png. This vulnerability is fixed in 5.13.2 and 4.16.18.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
astronpm
>= 5.0.0-alpha.0, < 5.13.25.13.2
@astrojs/nodenpm
< 9.1.19.1.1
astronpm
< 4.16.194.16.19

Affected products

1

Patches

1
4d16de7f95db

Merge commit from fork

https://github.com/withastro/astroEmanuele StoppaAug 15, 2025via ghsa
6 files changed · +97 2
  • .changeset/fresh-gifts-take.md+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +'@astrojs/internal-helpers': patch
    +'astro': patch
    +---
    +
    +Improves the detection of remote paths in the `_image` endpoint. Now `href` parameters that start with `//` are considered remote paths.
    
  • packages/astro/test/core-image.test.js+28 0 modified
    @@ -896,6 +896,24 @@ describe('astro:image', () => {
     			const img = await app.render(new Request(`https://example.com${src}`));
     			assert.equal(img.status, 200);
     		});
    +
    +		it('returns 403 when loading a relative pattern iamge', async () => {
    +			const fixtureWithBase = await loadFixture({
    +				root: './fixtures/core-image-ssr/',
    +				output: 'server',
    +				outDir: './dist/server-base-path',
    +				adapter: testAdapter(),
    +			});
    +			await fixtureWithBase.build();
    +			const app = await fixtureWithBase.loadTestAdapterApp();
    +			let request = new Request('http://example.com/');
    +			let response = await app.render(request);
    +			// making sure that the app works
    +			assert.equal(response.status, 200);
    +			request = new Request('http://example.com/_image/?href=//secure0x.netlify.app/secure0x.svg&f=svg');
    +			response = await app.render(request);
    +			assert.equal(response.status, 403);
    +		});
     	});
     
     	describe('build ssg', () => {
    @@ -1396,6 +1414,16 @@ describe('astro:image', () => {
     			assert.equal(src.startsWith('/_image?'), true);
     		});
     
    +		it("returns 403 for /_image when requesting a relative pattern image and the parameters aren't encoded", async () => {
    +			fixture = await loadFixture({
    +				root: './fixtures/core-image/',
    +			});
    +			devServer = await fixture.startDevServer();
    +			// we don't use `URLSearchParams` because the initial // will get encoded
    +			const response = await fixture.fetch('/_image?' + "href=//secure0x.netlify.app/secure0x.svg&f=svg");
    +			assert.equal(response.status, 403);
    +		})
    +
     		afterEach(async () => {
     			await devServer.stop();
     		});
    
  • packages/astro/test/fixtures/core-image/src/pages/empty.astro+16 0 added
    @@ -0,0 +1,16 @@
    +---
    +
    +---
    +
    +<html lang="en">
    +<head>
    +	<meta charset="utf-8" />
    +	<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    +	<meta name="viewport" content="width=device-width" />
    +	<meta name="generator" content={Astro.generator} />
    +	<title>Astro</title>
    +</head>
    +<body>
    +<h1>Astro</h1>
    +</body>
    +</html>
    
  • packages/internal-helpers/package.json+2 1 modified
    @@ -36,7 +36,8 @@
         "prepublish": "pnpm build",
         "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json",
         "build:ci": "astro-scripts build \"src/**/*.ts\"",
    -    "dev": "astro-scripts dev \"src/**/*.ts\""
    +    "dev": "astro-scripts dev \"src/**/*.ts\"",
    +    "test":  "astro-scripts test \"test/**/*.test.js\""
       },
       "devDependencies": {
         "astro-scripts": "workspace:*"
    
  • packages/internal-helpers/src/path.ts+21 1 modified
    @@ -101,8 +101,28 @@ export function removeQueryString(path: string) {
     	return index > 0 ? path.substring(0, index) : path;
     }
     
    +/**
    + * Regex that matches the following URLs like:
    + * - http://example.com
    + * - https://example.com
    + * - ftp://example.com
    + * - ws://example.com
    + * - //example.com (protocol-relative URLs)
    + */
    +const URL_PROTOCOL_REGEX = /^(?:(?:http|ftp|https|ws):?\/\/|\/\/)/;
    +
    +/**
    + * Checks whether the path is considered a remote path. Paths need to start with:
    + * - `http://`
    + * - `https://`
    + * - `ftp://`
    + * - `ws://`
    + * - `//` (protocol-relative URLs)
    + * - `data:` (base64 images)
    + * @param src
    + */
     export function isRemotePath(src: string) {
    -	return /^(?:http|ftp|https|ws):?\/\//.test(src) || src.startsWith('data:');
    +	return URL_PROTOCOL_REGEX.test(src) || src.startsWith('data:');
     }
     
     export function slash(path: string) {
    
  • packages/internal-helpers/test/path.test.js+24 0 added
    @@ -0,0 +1,24 @@
    +import assert from 'node:assert/strict';
    +import { describe, it } from 'node:test';
    +import { isRemotePath } from '../dist/path.js';
    +
    +describe('isRemotePath', () => {
    +	it("should return true if the path is remote", () => {
    +		assert.equal(isRemotePath('https://example.com/foo/bar.js'), true, "should be a remote path")
    +		assert.equal(isRemotePath('http://example.com/foo/bar.js'), true, "should be a remote path")
    +		assert.equal(isRemotePath('//example.com/foo/bar.js'), true, "should be a remote path")
    +		assert.equal(isRemotePath('ws://example.com/foo/bar.js'), true, "should be a remote path")
    +		assert.equal(isRemotePath('ftp://example.com/foo/bar.js'), true, "should be a remote path")
    +		assert.equal(isRemotePath('data:someCode'), true, "should be a remote path")
    +		// false
    +		assert.equal(isRemotePath('/local/path/file.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('relative/path/file.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('./relative/path/file.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('../relative/path/file.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('C:\\windows\\path\\file.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('file://example.com/foo/bar.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('sftp://example.com/foo/bar.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('wss://example.com/foo/bar.js'), false, "should not be a remote path")
    +		assert.equal(isRemotePath('mailto:example@example.com'), false, "should not be a remote path")
    +	})
    +});
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.