Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions tests/composition-runtime.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {fileURLToPath} from 'node:url';

// Exercise actual Worker fetch semantics using Wrangler's existing dev tools.
const require=createRequire(import.meta.url);
const tooling=createRequire(require.resolve('wrangler/package.json'));
const {Miniflare,convertV4MiniflareOptions}=tooling('miniflare');
const {buildSync}=tooling('esbuild');
const root=fileURLToPath(new URL('../',import.meta.url));
const script=buildSync({stdin:{contents:"import {compositionMedia} from './worker/src/meme-composition.mjs'; export default {fetch(){return compositionMedia('drake-preference');}}",resolveDir:root},bundle:true,write:false,format:'esm',platform:'browser'}).outputFiles[0].text;

test('actual Workerd media path serves allowed rasters and never follows redirects',async()=>{
let calls=0,redirect=false;
const runtime=new Miniflare(convertV4MiniflareOptions({
name:'caption-media-regression',modules:true,compatibilityDate:'2026-09-01',script,
outboundService:async request=>{
calls++;assert.equal(request.url,'https://i.imgflip.com/30b1gx.jpg');
return redirect?new Response(null,{status:302,headers:{Location:'https://private.invalid/'}}):
new Response(new Uint8Array([255,216,255,1]),{headers:{'Content-Type':'image/jpeg'}});
}
}));
try {
const image=await runtime.dispatchFetch('http://localhost/');
assert.equal(image.status,200);assert.equal(image.headers.get('content-type'),'image/jpeg');
assert.deepEqual(new Uint8Array(await image.arrayBuffer()),new Uint8Array([255,216,255,1]));
assert.equal(calls,1);
redirect=true;
assert.equal((await runtime.dispatchFetch('http://localhost/')).status,502);
assert.equal(calls,2,'redirect target must never be fetched');
}finally{await runtime.dispose();}
});
5 changes: 4 additions & 1 deletion tests/meme-composition.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,15 @@ test('body limit applies to bytes read, even when a caller omits Content-Length'
test('media fetch permits only catalogue rasters, rejects redirects and checks actual signatures',async()=>{
let fetched;
const response=await compositionMedia(drake.id,async(url,options)=>{fetched={url,options};return new Response(new Uint8Array([255,216,255,1]),{headers:{'Content-Type':'image/jpeg'}});});
assert.equal(response.status,200);assert.equal(fetched.url,drake.image_url);assert.equal(fetched.options.redirect,'error');
assert.equal(response.status,200);assert.equal(fetched.url,drake.image_url);assert.equal(fetched.options.redirect,'manual');
assert.equal(response.headers.get('Cross-Origin-Resource-Policy'),'same-origin');
for(const [type,bytes] of [['image/svg+xml','<svg></svg>'],['image/jpeg','<html>not an image</html>'],['image/gif','GIF89a']]) {
assert.equal((await compositionMedia(drake.id,async()=>new Response(bytes,{headers:{'Content-Type':type}}))).status,502);
}
assert.equal((await compositionMedia('https://localhost/admin',()=>assert.fail('Must not fetch.'))).status,404);
let redirects=0;
assert.equal((await compositionMedia(drake.id,async()=>{redirects++;return new Response(null,{status:302,headers:{Location:'https://private.invalid/'}});})).status,502);
assert.equal(redirects,1);
for(const url of ['https://i.imgflip.com.evil.test/a.jpg','https://user:pass@i.imgflip.com/a.jpg','https://i.imgflip.com:8443/a.jpg','http://i.imgflip.com/a.jpg','https://i.imgflip.com/../admin']) assert.equal(creationMediaUrl({image_url:url}),null);
});

Expand Down
14 changes: 11 additions & 3 deletions worker/src/meme-composition.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,22 @@ function isRaster(bytes,type) {
export async function compositionMedia(id,fetchImpl=fetch) {
const record=byId.get(id);const url=creationMediaUrl(record);
if(!url) return json({error:'Image composition is unavailable for this reference.'},404);
let stage='fetch',upstreamStatus;
try {
const response=await fetchImpl(url,{redirect:'error',signal:AbortSignal.timeout(12000)});
// Workerd rejects redirect:"error". Manual mode keeps redirects unfollowed;
// the response gate below rejects every non-2xx before reading any bytes.
const response=await fetchImpl(url,{redirect:'manual',signal:AbortSignal.timeout(12000)});
upstreamStatus=response.status;stage='response';
const type=response.headers.get('content-type')?.split(';')[0].toLowerCase();
if(!response.ok||!['image/jpeg','image/png','image/webp'].includes(type)||Number(response.headers.get('content-length'))>MAX_IMAGE_BYTES) {await response.body?.cancel();throw new Error();}
const bytes=await boundedBytes(response.body,MAX_IMAGE_BYTES);
stage='read';const bytes=await boundedBytes(response.body,MAX_IMAGE_BYTES);
stage='signature';
if(!isRaster(bytes,type)) throw new Error();
return new Response(bytes,{headers:{'Content-Type':type,'Cache-Control':'public, max-age=86400','X-Content-Type-Options':'nosniff','Cross-Origin-Resource-Policy':'same-origin','Referrer-Policy':'no-referrer'}});
} catch {return json({error:'Could not load the source image. Try again shortly.'},502);}
} catch {
console.log(JSON.stringify({event:'composition.media_failed',stage,upstream_status:upstreamStatus}));
return json({error:'Could not load the source image. Try again shortly.'},502);
}
}

export async function annaComposition(request,env) {
Expand Down
Loading