@@ -37,22 +37,94 @@ export default {
3737 }
3838
3939 if ( url . pathname . startsWith ( "/api/v1/image-proxy" ) ) {
40- const url = new URL ( request . url )
4140 const originalUrl = url . searchParams . get ( "url" )
4241 if ( ! originalUrl ) {
43- return new Response ( "Missing url" , { status : 400 } )
42+ return new Response ( "Missing url parameter " , { status : 400 } )
4443 }
4544
46- let cache = caches . default
47- const cacheKey = originalUrl
48- const cachedResponse = await cache . match ( cacheKey )
45+ // Security: Validate URL to prevent SSRF attacks
46+ const ALLOWED_DOMAINS = [ 'cdn.sanity.io' , 'sanity.io' ] ;
47+ let parsedUrl ;
48+ try {
49+ parsedUrl = new URL ( originalUrl ) ;
50+ // Only allow HTTPS URLs from Sanity domains
51+ if ( parsedUrl . protocol !== 'https:' ) {
52+ return new Response ( "Only HTTPS URLs are allowed" , { status : 403 } ) ;
53+ }
54+ const isAllowed = ALLOWED_DOMAINS . some ( domain =>
55+ parsedUrl . hostname === domain || parsedUrl . hostname . endsWith ( `.${ domain } ` )
56+ ) ;
57+ if ( ! isAllowed ) {
58+ return new Response ( "URL not allowed" , { status : 403 } ) ;
59+ }
60+ } catch ( error ) {
61+ return new Response ( "Invalid URL" , { status : 400 } ) ;
62+ }
63+
64+ // Use cache with proper key
65+ const cache = caches . default ;
66+ const cacheKey = new Request ( originalUrl , { method : 'GET' } ) ;
67+ const cachedResponse = await cache . match ( cacheKey ) ;
4968 if ( cachedResponse ) {
50- return cachedResponse
69+ // Add cache hit header
70+ const headers = new Headers ( cachedResponse . headers ) ;
71+ headers . set ( 'X-Cache' , 'HIT' ) ;
72+ return new Response ( cachedResponse . body , {
73+ status : cachedResponse . status ,
74+ headers
75+ } ) ;
5176 }
5277
53- const response = await fetch ( originalUrl , request )
54- cache . put ( cacheKey , response . clone ( ) )
55- return response
78+ // Fetch with timeout and error handling
79+ try {
80+ const controller = new AbortController ( ) ;
81+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 10000 ) ; // 10 second timeout
82+
83+ const response = await fetch ( originalUrl , {
84+ signal : controller . signal ,
85+ headers : {
86+ 'User-Agent' : 'CloudflareWorkers-ImageProxy/1.0'
87+ }
88+ } ) ;
89+
90+ clearTimeout ( timeoutId ) ;
91+
92+ // Validate response
93+ if ( ! response . ok ) {
94+ return new Response ( `Upstream error: ${ response . status } ` , {
95+ status : response . status
96+ } ) ;
97+ }
98+
99+ // Validate content type is an image
100+ const contentType = response . headers . get ( 'content-type' ) ;
101+ if ( ! contentType || ! contentType . startsWith ( 'image/' ) ) {
102+ return new Response ( "Invalid content type - only images are allowed" , {
103+ status : 400
104+ } ) ;
105+ }
106+
107+ // Add cache headers
108+ const headers = new Headers ( response . headers ) ;
109+ headers . set ( 'Cache-Control' , 'public, max-age=86400' ) ; // 24 hours
110+ headers . set ( 'X-Cache' , 'MISS' ) ;
111+
112+ const newResponse = new Response ( response . body , {
113+ status : response . status ,
114+ headers
115+ } ) ;
116+
117+ // Store in cache
118+ ctx . waitUntil ( cache . put ( cacheKey , newResponse . clone ( ) ) ) ;
119+
120+ return newResponse ;
121+ } catch ( error ) {
122+ console . error ( 'Image proxy error:' , error ) ;
123+ if ( error . name === 'AbortError' ) {
124+ return new Response ( "Request timeout" , { status : 504 } ) ;
125+ }
126+ return new Response ( "Proxy error" , { status : 500 } ) ;
127+ }
56128 }
57129
58130
0 commit comments