@@ -8,6 +8,7 @@ import https from 'https'
88import net from 'net'
99
1010const each = [ 'http' , 'https' ] as const
11+ const authorizedOrigin = 'https://extensions.shopifycdn.com'
1112
1213describe . sequential . each ( each ) ( 'http-reverse-proxy for %s' , ( protocol ) => {
1314 const test = getTestReverseProxy ( protocol )
@@ -55,34 +56,58 @@ describe.sequential.each(each)('http-reverse-proxy for %s', (protocol) => {
5556 } )
5657 } )
5758
58- test ( 'responds to CORS preflight OPTIONS with default headers ' , { retry : 2 } , async ( { setup} ) => {
59+ test ( 'forwards an authorized credentialed CORS preflight to the target ' , { retry : 2 } , async ( { setup} ) => {
5960 const response = await fetch ( `${ protocol } ://localhost:${ setup . proxyPort } /path1/test` , {
6061 method : 'OPTIONS' ,
6162 headers : {
62- Origin : 'https://extensions.shopifycdn.com' ,
63- 'Access-Control-Request-Method' : 'GET ' ,
64- 'Access-Control-Request-Headers' : 'Authorization ' ,
63+ Origin : authorizedOrigin ,
64+ 'Access-Control-Request-Method' : 'POST ' ,
65+ 'Access-Control-Request-Headers' : 'Content-Type ' ,
6566 } ,
6667 agent,
6768 } )
6869 expect ( response . status ) . toBe ( 204 )
69- expect ( response . headers . get ( 'access-control-allow-origin ' ) ) . toBe ( 'https://extensions.shopifycdn.com ' )
70- expect ( response . headers . get ( 'access-control-allow-methods ' ) ) . toBe ( 'GET' )
71- expect ( response . headers . get ( 'access-control-allow-headers ' ) ) . toBe ( 'Authorization ' )
72- expect ( response . headers . get ( 'access-control-max-age ' ) ) . toBe ( '86400 ' )
70+ expect ( response . headers . get ( 'x-preflight-handled-by ' ) ) . toBe ( 'target ' )
71+ expect ( response . headers . get ( 'access-control-allow-origin ' ) ) . toBe ( authorizedOrigin )
72+ expect ( response . headers . get ( 'access-control-allow-methods ' ) ) . toBe ( 'POST ' )
73+ expect ( response . headers . get ( 'access-control-allow-headers ' ) ) . toBe ( 'Content-Type ' )
7374 expect ( response . headers . get ( 'access-control-allow-credentials' ) ) . toBe ( 'true' )
7475 } )
7576
76- test ( 'responds to CORS preflight OPTIONS with defaults when no request headers' , { retry : 2 } , async ( { setup} ) => {
77- const response = await fetch ( `${ protocol } ://localhost:${ setup . proxyPort } /path1/test` , {
78- method : 'OPTIONS' ,
79- agent,
80- } )
77+ test ( 'does not authorize credentials for untrusted origins' , { retry : 2 } , async ( { setup} ) => {
78+ const untrustedOrigins = [
79+ 'https://evil.example' ,
80+ 'null' ,
81+ 'not a valid origin' ,
82+ 'http://extensions.shopifycdn.com' ,
83+ 'https://extensions.shopifycdn.com:3000' ,
84+ ]
85+
86+ await Promise . all (
87+ untrustedOrigins . map ( async ( origin ) => {
88+ const response = await fetch ( `${ protocol } ://localhost:${ setup . proxyPort } /path1/test` , {
89+ method : 'OPTIONS' ,
90+ headers : {
91+ Origin : origin ,
92+ 'Access-Control-Request-Method' : 'POST' ,
93+ 'Access-Control-Request-Headers' : 'Content-Type' ,
94+ } ,
95+ agent,
96+ } )
97+ expect ( response . status ) . toBe ( 204 )
98+ expect ( response . headers . get ( 'access-control-allow-origin' ) ) . toBeNull ( )
99+ expect ( response . headers . get ( 'access-control-allow-credentials' ) ) . toBeNull ( )
100+ expect ( response . headers . get ( 'x-preflight-handled-by' ) ) . toBe ( 'target' )
101+ } ) ,
102+ )
103+ } )
104+
105+ test ( 'forwards OPTIONS without an Origin without authorizing credentials' , { retry : 2 } , async ( { setup} ) => {
106+ const response = await fetch ( `${ protocol } ://localhost:${ setup . proxyPort } /path1/test` , { method : 'OPTIONS' , agent} )
81107 expect ( response . status ) . toBe ( 204 )
82- expect ( response . headers . get ( 'access-control-allow-origin' ) ) . toBe ( '*' )
83- expect ( response . headers . get ( 'access-control-allow-methods' ) ) . toBe ( 'GET, POST, PUT, DELETE, PATCH, OPTIONS' )
84- expect ( response . headers . get ( 'access-control-allow-headers' ) ) . toBe ( 'Content-Type, Authorization' )
108+ expect ( response . headers . get ( 'access-control-allow-origin' ) ) . toBeNull ( )
85109 expect ( response . headers . get ( 'access-control-allow-credentials' ) ) . toBeNull ( )
110+ expect ( response . headers . get ( 'x-preflight-handled-by' ) ) . toBe ( 'target' )
86111 } )
87112
88113 test ( 'closes the server when aborted' , async ( { setup} ) => {
@@ -115,6 +140,21 @@ function getTestReverseProxy(protocol: 'http' | 'https') {
115140 // eslint-disable-next-line no-empty-pattern
116141 setup : async ( { } , use ) => {
117142 const targetServer1 = http . createServer ( ( req , res ) => {
143+ if ( req . method === 'OPTIONS' ) {
144+ const origin = req . headers . origin
145+ res . writeHead ( 204 , {
146+ 'X-Preflight-Handled-By' : 'target' ,
147+ ...( origin === authorizedOrigin
148+ ? {
149+ 'Access-Control-Allow-Origin' : origin ,
150+ 'Access-Control-Allow-Credentials' : 'true' ,
151+ 'Access-Control-Allow-Methods' : req . headers [ 'access-control-request-method' ] ?? '' ,
152+ 'Access-Control-Allow-Headers' : req . headers [ 'access-control-request-headers' ] ?? '' ,
153+ }
154+ : { } ) ,
155+ } )
156+ return res . end ( )
157+ }
118158 res . writeHead ( 200 , { 'Content-Type' : 'text/plain' } )
119159 res . end ( 'Response from target server 1' )
120160 } )
0 commit comments