Skip to content

Commit 1a2023e

Browse files
committed
fix(assets): skip allowlist for data URIs, restore incorrectly modified tests
- Skip domain allowlist check for data: URIs (inline content, not remote fetches) - Restore CSP-compliant position/style tests that were incorrectly rewritten - Restore deleted format detection tests (SVG, data URI, extensionless URL) - Add images.unsplash.com to Netlify missing-dimension fixture so MissingImageDimension fires before RemoteImageNotAllowed
1 parent b2e1730 commit 1a2023e

3 files changed

Lines changed: 64 additions & 10 deletions

File tree

packages/astro/src/assets/internal.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,13 @@ export async function getImage(
7878
src: await resolveSrc(options.src),
7979
};
8080

81-
// Check remote image allowlist for all remote images, not just inferSize
82-
if (isRemoteImage(resolvedOptions.src) && isRemotePath(resolvedOptions.src)) {
81+
// Check remote image allowlist for all remote images, not just inferSize.
82+
// Skip data: URIs — they are inline content, not remote fetches.
83+
if (
84+
isRemoteImage(resolvedOptions.src) &&
85+
isRemotePath(resolvedOptions.src) &&
86+
!resolvedOptions.src.startsWith('data:')
87+
) {
8388
if (!isRemoteAllowed(resolvedOptions.src, imageConfig)) {
8489
throw new AstroError({
8590
...AstroErrorData.RemoteImageNotAllowed,

packages/astro/test/units/assets/getImage.test.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ describe('getImage', () => {
280280
assert.equal(result.attributes.position, undefined);
281281
});
282282

283-
it('includes object-position in style attribute when position is provided', async () => {
283+
it('does not add inline style for position (CSP compliance)', async () => {
284284
const result = await renderImage({
285285
src: 'https://example.com/photo.jpg',
286286
width: 300,
@@ -290,10 +290,24 @@ describe('getImage', () => {
290290
position: 'left top',
291291
});
292292

293-
assert.match(result.attributes.style, /object-position:\s*left top/);
293+
// Position should only live in the data attribute, not in inline styles
294+
assert.equal(result.attributes['data-astro-image-pos'], 'left-top');
295+
const style = result.attributes.style;
296+
if (typeof style === 'string') {
297+
assert.ok(
298+
!style.includes('object-position'),
299+
'inline style should not contain object-position',
300+
);
301+
} else if (typeof style === 'object' && style !== null) {
302+
assert.equal(
303+
'objectPosition' in style,
304+
false,
305+
'style object should not contain objectPosition',
306+
);
307+
}
294308
});
295309

296-
it('merges position into existing style object without overwriting', async () => {
310+
it('preserves user-provided style without injecting position', async () => {
297311
const result = await renderImage({
298312
src: 'https://example.com/photo.jpg',
299313
width: 300,
@@ -304,15 +318,14 @@ describe('getImage', () => {
304318
style: { color: 'red' },
305319
});
306320

307-
assert.deepStrictEqual(result.attributes.style, {
308-
color: 'red',
309-
objectPosition: 'top right',
310-
});
321+
// User style should be preserved as-is, position only in data attribute
322+
assert.equal(result.attributes['data-astro-image-pos'], 'top-right');
323+
assert.deepStrictEqual(result.attributes.style, { color: 'red' });
311324
});
312325
});
313326

314327
describe('format', () => {
315-
it('defaults to webp format', async () => {
328+
it('defaults to webp for remote images with a non-svg extension', async () => {
316329
const result = await renderImage({
317330
src: 'https://example.com/photo.jpg',
318331
width: 800,
@@ -324,6 +337,41 @@ describe('getImage', () => {
324337
assert.equal(params.get('f'), 'webp');
325338
});
326339

340+
it('defaults to svg for remote URLs ending in .svg so they pass through', async () => {
341+
const result = await renderImage({
342+
src: 'https://example.com/icon.svg',
343+
width: 64,
344+
height: 64,
345+
alt: 'Format test',
346+
});
347+
const params = new URL(result.src, 'http://localhost').searchParams;
348+
assert.equal(params.get('f'), 'svg');
349+
});
350+
351+
it('defaults to svg for data:image/svg+xml so they pass through', async () => {
352+
// Data URIs aren't in the test allowlist, so the URL is returned as-is by getURL — verify
353+
// the resolved transform options instead.
354+
const svg = 'data:image/svg+xml,%3Csvg/%3E';
355+
const result = await renderImage({
356+
src: svg,
357+
width: 32,
358+
height: 32,
359+
alt: 'Format test',
360+
});
361+
assert.equal(result.options.format, 'svg');
362+
});
363+
364+
it('omits format param for remote URLs without a detectable extension (resolved by /_image at request time)', async () => {
365+
const result = await renderImage({
366+
src: 'https://example.com/api/avatar',
367+
width: 64,
368+
height: 64,
369+
alt: 'Format test',
370+
});
371+
const params = new URL(result.src, 'http://localhost').searchParams;
372+
assert.equal(params.has('f'), false);
373+
});
374+
327375
it('respects explicit format', async () => {
328376
const result = await renderImage({
329377
src: 'https://example.com/photo.jpg',

packages/integrations/netlify/test/static/fixtures/image-missing-dimension/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default defineConfig({
55
adapter: netlify(),
66
output: 'static',
77
image: {
8+
domains: ['images.unsplash.com'],
89
service: {
910
entrypoint: 'astro/assets/services/sharp'
1011
}

0 commit comments

Comments
 (0)