sitesmith cleaner: drop images.pexels.com from the AI image host allow-list
Stock photos are imported into the site's assets, never hotlinked, so the only external image host model output may use is placehold.co. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185ukXgk6UunFCKyXqLe2Cq
This commit is contained in:
1 parent
94bbfd3ec9
commit
da860e2dae
3 files changed
+11
-11
No files matched your search
@@ -28,7 +28,7 @@ fs.rmSync(tmp, { force: true });
|
||||
|
||||
const SITE = 'http://site.test';
|
||||
// The published page's own head resources (fonts, Font Awesome) + the cleaner's image hosts.
|
||||
const ALLOWED_HOSTS = new Set(['site.test', 'fonts.googleapis.com', 'fonts.gstatic.com', 'cdnjs.cloudflare.com', 'images.pexels.com', 'placehold.co']);
|
||||
const ALLOWED_HOSTS = new Set(['site.test', 'fonts.googleapis.com', 'fonts.gstatic.com', 'cdnjs.cloudflare.com', 'placehold.co']);
|
||||
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64');
|
||||
|
||||
let pass = 0; let fail = 0;
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('allowUrl: tricky link forms', () => {
|
||||
expect(allowUrl('link', 'HTTPS://example.com/')).toBe('HTTPS://example.com/');
|
||||
});
|
||||
test('the image and video rules reject the same tricky forms', () => {
|
||||
for (const v of ['//images.pexels.com/a.jpg', 'HTTPS://example.com/a.jpg', 'assets/a\\b.jpg', 'assets/' + 'a'.repeat(250)]) {
|
||||
for (const v of ['//placehold.co/a.jpg', 'HTTPS://example.com/a.jpg', 'assets/a\\b.jpg', 'assets/' + 'a'.repeat(250)]) {
|
||||
expect(allowUrl('image', v), v).toBeNull();
|
||||
expect(allowUrl('video', v), v).toBeNull();
|
||||
}
|
||||
@@ -39,11 +39,10 @@ describe('allowUrl: tricky link forms', () => {
|
||||
});
|
||||
|
||||
describe('allowUrl: image and video props', () => {
|
||||
test('images: own assets and the supported stock/placeholder hosts only', () => {
|
||||
test('images: own assets and the placeholder host only (stock photos are imported to assets/, never hotlinked)', () => {
|
||||
expect(allowUrl('image', 'assets/hero.jpg')).toBe('assets/hero.jpg');
|
||||
expect(allowUrl('image', 'https://images.pexels.com/photos/1/a.jpeg')).toBe('https://images.pexels.com/photos/1/a.jpeg');
|
||||
expect(allowUrl('image', 'https://placehold.co/600x400?text=Hero')).toBe('https://placehold.co/600x400?text=Hero');
|
||||
for (const bad of ['https://example.com/a.png', 'http://images.pexels.com/a.jpg', 'data:image/png;base64,AAAA', '/api/assets-proxy/?site_id=1&filename=a.jpg', 'assets/../x.jpg', 'images/a.jpg']) {
|
||||
for (const bad of ['https://example.com/a.png', 'http://placehold.co/a.jpg', 'https://images.pexels.com/photos/1/a.jpeg', 'https://cdn.pixabay.com/photo/a.jpg', 'https://pixabay.com/get/a.jpg', 'data:image/png;base64,AAAA', '/api/assets-proxy/?site_id=1&filename=a.jpg', 'assets/../x.jpg', 'images/a.jpg']) {
|
||||
expect(allowUrl('image', bad), bad).toBeNull();
|
||||
}
|
||||
});
|
||||
@@ -151,7 +150,7 @@ describe('patches are cleaned with the TARGET component\'s rules (review finding
|
||||
test('ButtonLink href and ImageBlock src', () => {
|
||||
expect(propsOf(cleanAiResponse(patch({ href: 'https://example.com/' }), as('ButtonLink')))).toEqual({ href: 'https://example.com/' });
|
||||
expect(propsOf(cleanAiResponse(patch({ href: 'javascript:x' }), as(undefined)))).toEqual({});
|
||||
const px = 'https://images.pexels.com/photos/1/a.jpeg';
|
||||
const px = 'https://placehold.co/600x400?text=x';
|
||||
expect(propsOf(cleanAiResponse(patch({ src: px }), as('ImageBlock')))).toEqual({ src: px });
|
||||
expect(propsOf(cleanAiResponse(patch({ src: px }), as(undefined)))).toEqual({});
|
||||
expect(propsOf(cleanAiResponse(patch({ src: 'https://example.com/a.png' }), as('ImageBlock')))).toEqual({});
|
||||
@@ -161,7 +160,7 @@ describe('patches are cleaned with the TARGET component\'s rules (review finding
|
||||
.toEqual({ logoUrl: '/', logoImage: 'assets/logo.png', links: [{ text: 'A', href: '#a' }, { text: 'B' }] });
|
||||
});
|
||||
test('a componentOf that throws is treated as unresolvable (strict)', () => {
|
||||
expect(propsOf(cleanAiResponse(patch({ code: 'x', src: 'https://images.pexels.com/a.jpg' }), () => { throw new Error('gone'); }))).toEqual({});
|
||||
expect(propsOf(cleanAiResponse(patch({ code: 'x', src: 'https://placehold.co/a.jpg' }), () => { throw new Error('gone'); }))).toEqual({});
|
||||
});
|
||||
test('replace_node / insert_* trees carry their component names', () => {
|
||||
const r = cleanAiResponse({ type: 'patch', message: 'm', ops: [
|
||||
@@ -208,7 +207,7 @@ describe('placehold.co backgrounds are label-free', () => {
|
||||
const img = 'https://placehold.co/800x600/eeeeee/333333?text=Team+photo';
|
||||
expect(cleanAiProps({ src: img }, 'ImageBlock').src).toBe(img);
|
||||
expect(cleanAiProps({ bgImage: 'assets/hero.jpg' }, 'BackgroundSection').bgImage).toBe('assets/hero.jpg');
|
||||
expect(cleanAiProps({ bgImage: 'https://images.pexels.com/photos/1/a.jpg?text=x' }, 'BackgroundSection').bgImage).toBe('https://images.pexels.com/photos/1/a.jpg?text=x');
|
||||
expect(cleanAiProps({ bgImage: 'https://images.pexels.com/photos/1/a.jpg' }, 'BackgroundSection').bgImage).toBeUndefined(); // stock hosts are no longer allowed
|
||||
});
|
||||
test('round 6 (live case 4): the label is a zero-width space, so placehold.co draws NO glyphs at all (measured: same-colour "W x H" glyphs still antialias into a ghost label; ?text=%E2%80%8B returns a bare <rect>)', () => {
|
||||
const out = cleanAiProps({ bgImage: LIVE }, 'BackgroundSection').bgImage as string;
|
||||
|
||||
@@ -27,9 +27,10 @@
|
||||
import { LABEL_FREE_TEXT, placeholderBoxHex } from './placeholder-bg';
|
||||
import type { SitesmithResponse, SerializedTreeNode, SitesmithPatchOp } from '../types/sitesmith';
|
||||
|
||||
/** Image hosts model output may reference: the stock provider the builder imports from
|
||||
* (SiteBuilderStock::IMAGE_HOST) and the placeholder host the Sitesmith prompt prescribes. */
|
||||
export const AI_IMAGE_HOSTS: readonly string[] = ['images.pexels.com', 'placehold.co'];
|
||||
/** Image hosts model output may reference: only the placeholder host the Sitesmith prompt prescribes.
|
||||
* Stock photos are never hotlinked (Pixabay forbids permanent hotlinking): they are imported into the
|
||||
* site's own `assets/` by the Stock tab, so no stock-provider host is allowed here. */
|
||||
export const AI_IMAGE_HOSTS: readonly string[] = ['placehold.co'];
|
||||
/** Video hosts VideoBlock turns into embeds. */
|
||||
export const AI_VIDEO_HOSTS: readonly string[] = ['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be', 'www.youtube-nocookie.com', 'vimeo.com', 'www.vimeo.com', 'player.vimeo.com'];
|
||||
|
||||
|
||||
Reference in new issue
Block a user