@@ -395,6 +395,48 @@ describe("Local Explorer works with custom routes", () => {
395395 expect ( res . status ) . toBe ( 403 ) ;
396396 await res . arrayBuffer ( ) ;
397397 } ) ;
398+
399+ // An exact configured route must not authorize subdomains of that route.
400+ // Subdomain matching should only apply to wildcard routes.
401+ test ( "blocks Origin that is a subdomain of an exact configured route" , async ( {
402+ expect,
403+ } ) => {
404+ const res = await mf . dispatchFetch ( `${ BASE_URL } /storage/kv/namespaces` , {
405+ headers : { Origin : "https://sub.my-custom-site.com" } ,
406+ } ) ;
407+ expect ( res . status ) . toBe ( 403 ) ;
408+ await res . arrayBuffer ( ) ;
409+ } ) ;
410+
411+ test ( "blocks Host that is a subdomain of an exact configured route" , async ( {
412+ expect,
413+ } ) => {
414+ const url = await mf . ready ;
415+ const status = await new Promise < number > ( ( resolve , reject ) => {
416+ const req = http . get (
417+ `${ url . origin } ${ CorePaths . EXPLORER } /api/storage/kv/namespaces` ,
418+ { setHost : false , headers : { Host : "sub.my-custom-site.com" } } ,
419+ ( res ) => {
420+ res . resume ( ) ;
421+ resolve ( res . statusCode ?? 0 ) ;
422+ }
423+ ) ;
424+ req . on ( "error" , reject ) ;
425+ } ) ;
426+ expect ( status ) . toBe ( 403 ) ;
427+ } ) ;
428+
429+ test ( "blocks look-alike hostname that ends with the exact route" , async ( {
430+ expect,
431+ } ) => {
432+ // Guards against a naive endsWith() bypass: `evilmy-custom-site.com`
433+ // should NOT be accepted just because the suffix matches.
434+ const res = await mf . dispatchFetch ( `${ BASE_URL } /storage/kv/namespaces` , {
435+ headers : { Origin : "https://evilmy-custom-site.com" } ,
436+ } ) ;
437+ expect ( res . status ) . toBe ( 403 ) ;
438+ await res . arrayBuffer ( ) ;
439+ } ) ;
398440} ) ;
399441
400442describe ( "Local Explorer works with wildcard routes" , ( ) => {
@@ -433,6 +475,107 @@ describe("Local Explorer works with wildcard routes", () => {
433475 expect ( res . status ) . toBe ( 200 ) ;
434476 await res . arrayBuffer ( ) ;
435477 } ) ;
478+
479+ test ( "allows Origin that is a deep subdomain of a wildcard route" , async ( {
480+ expect,
481+ } ) => {
482+ const res = await mf . dispatchFetch ( `${ BASE_URL } /storage/kv/namespaces` , {
483+ headers : { Origin : "https://a.b.example.com" } ,
484+ } ) ;
485+ expect ( res . status ) . toBe ( 200 ) ;
486+ await res . arrayBuffer ( ) ;
487+ } ) ;
488+
489+ test ( "blocks look-alike sibling hostname not under the wildcard route" , async ( {
490+ expect,
491+ } ) => {
492+ // `notexample.com` ends with `example.com` but is not a subdomain of it.
493+ // Guards against a naive endsWith() match without a dot boundary.
494+ const res = await mf . dispatchFetch ( `${ BASE_URL } /storage/kv/namespaces` , {
495+ headers : { Origin : "https://notexample.com" } ,
496+ } ) ;
497+ expect ( res . status ) . toBe ( 403 ) ;
498+ await res . arrayBuffer ( ) ;
499+ } ) ;
500+ } ) ;
501+
502+ // The configured `upstream` hostname is a single host, not a wildcard.
503+ // Subdomains of the upstream hostname must not be authorized for /cdn-cgi/*
504+ // requests.
505+ describe ( "Local Explorer works with upstream" , ( ) => {
506+ let mf : Miniflare ;
507+
508+ beforeAll ( async ( ) => {
509+ mf = new Miniflare ( {
510+ inspectorPort : 0 ,
511+ compatibilityDate : "2025-01-01" ,
512+ modules : true ,
513+ script : `export default { fetch() { return new Response("user worker"); } }` ,
514+ unsafeLocalExplorer : true ,
515+ kvNamespaces : {
516+ TEST_KV : "test-kv-id" ,
517+ } ,
518+ upstream : "https://upstream-host.example/" ,
519+ } ) ;
520+ } ) ;
521+
522+ afterAll ( async ( ) => {
523+ await disposeWithRetry ( mf ) ;
524+ } ) ;
525+
526+ test ( "allows configured upstream host exactly" , async ( { expect } ) => {
527+ const url = await mf . ready ;
528+ const status = await new Promise < number > ( ( resolve , reject ) => {
529+ const req = http . get (
530+ `${ url . origin } ${ CorePaths . EXPLORER } /api/storage/kv/namespaces` ,
531+ { setHost : false , headers : { Host : "upstream-host.example" } } ,
532+ ( res ) => {
533+ res . resume ( ) ;
534+ resolve ( res . statusCode ?? 0 ) ;
535+ }
536+ ) ;
537+ req . on ( "error" , reject ) ;
538+ } ) ;
539+ expect ( status ) . toBe ( 200 ) ;
540+ } ) ;
541+
542+ test ( "blocks subdomain of configured upstream host (Host header)" , async ( {
543+ expect,
544+ } ) => {
545+ const url = await mf . ready ;
546+ const status = await new Promise < number > ( ( resolve , reject ) => {
547+ const req = http . get (
548+ `${ url . origin } ${ CorePaths . EXPLORER } /api/storage/kv/namespaces` ,
549+ { setHost : false , headers : { Host : "sub.upstream-host.example" } } ,
550+ ( res ) => {
551+ res . resume ( ) ;
552+ resolve ( res . statusCode ?? 0 ) ;
553+ }
554+ ) ;
555+ req . on ( "error" , reject ) ;
556+ } ) ;
557+ expect ( status ) . toBe ( 403 ) ;
558+ } ) ;
559+
560+ test ( "blocks subdomain of configured upstream host (Origin header)" , async ( {
561+ expect,
562+ } ) => {
563+ const res = await mf . dispatchFetch ( `${ BASE_URL } /storage/kv/namespaces` , {
564+ headers : { Origin : "https://sub.upstream-host.example" } ,
565+ } ) ;
566+ expect ( res . status ) . toBe ( 403 ) ;
567+ await res . arrayBuffer ( ) ;
568+ } ) ;
569+
570+ test ( "blocks unrelated external host even with upstream configured" , async ( {
571+ expect,
572+ } ) => {
573+ const res = await mf . dispatchFetch ( `${ BASE_URL } /storage/kv/namespaces` , {
574+ headers : { Origin : "https://evil.com" } ,
575+ } ) ;
576+ expect ( res . status ) . toBe ( 403 ) ;
577+ await res . arrayBuffer ( ) ;
578+ } ) ;
436579} ) ;
437580
438581describe ( "Local Explorer /api/local/workers endpoint" , ( ) => {
0 commit comments