@@ -39,6 +39,7 @@ pub struct ServeState {
3939 token : Arc < str > ,
4040 allowed_hosts : Arc < HashSet < String > > ,
4141 allowed_origins : Arc < HashSet < String > > ,
42+ wildcard_bind : bool ,
4243 subprocess_limit : Arc < Semaphore > ,
4344}
4445
@@ -140,6 +141,7 @@ pub fn build_router(app_state: Arc<AppState>, options: &ServeOptions, token: Str
140141 token : Arc :: from ( token) ,
141142 allowed_hosts : Arc :: new ( allowed_hosts) ,
142143 allowed_origins : Arc :: new ( allowed_origins) ,
144+ wildcard_bind : options. host . is_unspecified ( ) ,
143145 subprocess_limit : Arc :: new ( Semaphore :: new ( SUBPROCESS_CONCURRENCY_LIMIT ) ) ,
144146 } ;
145147
@@ -386,27 +388,44 @@ fn allowed_origins(hosts: &HashSet<String>) -> HashSet<String> {
386388}
387389
388390fn host_allowed ( headers : & HeaderMap , state : & ServeState ) -> bool {
389- let Some ( host) = headers
390- . get ( header:: HOST )
391- . and_then ( |value| value. to_str ( ) . ok ( ) )
392- else {
391+ let Some ( host) = request_host ( headers) else {
393392 return false ;
394393 } ;
395- state
396- . allowed_hosts
397- . contains ( & host. trim ( ) . to_ascii_lowercase ( ) )
394+ if state. wildcard_bind {
395+ // A wildcard bind can be reached through any local interface address, so a fixed
396+ // Host list would reject the operator's actual LAN URL.
397+ return true ;
398+ }
399+ state. allowed_hosts . contains ( & host)
398400}
399401
400402fn origin_allowed ( headers : & HeaderMap , state : & ServeState ) -> bool {
401403 let Some ( origin) = headers
402404 . get ( header:: ORIGIN )
403405 . and_then ( |value| value. to_str ( ) . ok ( ) )
406+ . map ( normalize_header_value)
404407 else {
405408 return true ;
406409 } ;
407- state
408- . allowed_origins
409- . contains ( & origin. trim ( ) . to_ascii_lowercase ( ) )
410+ if state. wildcard_bind {
411+ let Some ( host) = request_host ( headers) else {
412+ return false ;
413+ } ;
414+ return origin == format ! ( "http://{host}" ) ;
415+ }
416+ state. allowed_origins . contains ( & origin)
417+ }
418+
419+ fn request_host ( headers : & HeaderMap ) -> Option < String > {
420+ headers
421+ . get ( header:: HOST )
422+ . and_then ( |value| value. to_str ( ) . ok ( ) )
423+ . map ( normalize_header_value)
424+ . filter ( |value| !value. is_empty ( ) )
425+ }
426+
427+ fn normalize_header_value ( value : & str ) -> String {
428+ value. trim ( ) . to_ascii_lowercase ( )
410429}
411430
412431pub ( crate ) fn authorization_valid ( headers : & HeaderMap , expected_token : & str ) -> bool {
@@ -563,14 +582,22 @@ mod tests {
563582 }
564583
565584 async fn spawn_test_server ( ) -> TestServer {
585+ spawn_test_server_with_options ( DEFAULT_HOST , false ) . await
586+ }
587+
588+ async fn spawn_remote_test_server ( ) -> TestServer {
589+ spawn_test_server_with_options ( IpAddr :: V4 ( Ipv4Addr :: UNSPECIFIED ) , true ) . await
590+ }
591+
592+ async fn spawn_test_server_with_options ( host : IpAddr , allow_remote : bool ) -> TestServer {
566593 let listener = tokio:: net:: TcpListener :: bind ( ( Ipv4Addr :: LOCALHOST , 0 ) )
567594 . await
568595 . expect ( "test listener" ) ;
569596 let port = listener. local_addr ( ) . expect ( "local addr" ) . port ( ) ;
570597 let options = ServeOptions {
571- host : DEFAULT_HOST ,
598+ host,
572599 port,
573- allow_remote : false ,
600+ allow_remote,
574601 } ;
575602 let token = "a" . repeat ( 64 ) ;
576603 let app = build_router ( Arc :: new ( state_without_db ( ) ) , & options, token. clone ( ) ) ;
@@ -844,6 +871,71 @@ mod tests {
844871 }
845872 }
846873
874+ #[ tokio:: test]
875+ async fn route_accepts_loopback_host_header ( ) {
876+ let server = spawn_test_server ( ) . await ;
877+ let response = reqwest:: Client :: new ( )
878+ . post ( format ! ( "{}/api/invoke" , server. base_url) )
879+ . bearer_auth ( & server. token )
880+ . header ( reqwest:: header:: HOST , format ! ( "127.0.0.1:{}" , server. port) )
881+ . json ( & json ! ( { "cmd" : "get_db_health" , "args" : { } } ) )
882+ . send ( )
883+ . await
884+ . expect ( "response" ) ;
885+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
886+ }
887+
888+ #[ tokio:: test]
889+ async fn remote_route_accepts_wildcard_host_without_origin ( ) {
890+ let server = spawn_remote_test_server ( ) . await ;
891+ let response = reqwest:: Client :: new ( )
892+ . post ( format ! ( "{}/api/invoke" , server. base_url) )
893+ . bearer_auth ( & server. token )
894+ . header (
895+ reqwest:: header:: HOST ,
896+ format ! ( "192.168.1.5:{}" , server. port) ,
897+ )
898+ . json ( & json ! ( { "cmd" : "get_db_health" , "args" : { } } ) )
899+ . send ( )
900+ . await
901+ . expect ( "response" ) ;
902+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
903+ }
904+
905+ #[ tokio:: test]
906+ async fn remote_route_rejects_cross_origin_for_wildcard_host ( ) {
907+ let server = spawn_remote_test_server ( ) . await ;
908+ let response = reqwest:: Client :: new ( )
909+ . post ( format ! ( "{}/api/invoke" , server. base_url) )
910+ . bearer_auth ( & server. token )
911+ . header (
912+ reqwest:: header:: HOST ,
913+ format ! ( "192.168.1.5:{}" , server. port) ,
914+ )
915+ . header ( reqwest:: header:: ORIGIN , "http://evil.test" )
916+ . json ( & json ! ( { "cmd" : "get_db_health" , "args" : { } } ) )
917+ . send ( )
918+ . await
919+ . expect ( "response" ) ;
920+ assert_eq ! ( response. status( ) , StatusCode :: FORBIDDEN ) ;
921+ }
922+
923+ #[ tokio:: test]
924+ async fn remote_route_accepts_same_origin_for_wildcard_host ( ) {
925+ let server = spawn_remote_test_server ( ) . await ;
926+ let host = format ! ( "192.168.1.5:{}" , server. port) ;
927+ let response = reqwest:: Client :: new ( )
928+ . post ( format ! ( "{}/api/invoke" , server. base_url) )
929+ . bearer_auth ( & server. token )
930+ . header ( reqwest:: header:: HOST , & host)
931+ . header ( reqwest:: header:: ORIGIN , format ! ( "http://{host}" ) )
932+ . json ( & json ! ( { "cmd" : "get_db_health" , "args" : { } } ) )
933+ . send ( )
934+ . await
935+ . expect ( "response" ) ;
936+ assert_eq ! ( response. status( ) , StatusCode :: OK ) ;
937+ }
938+
847939 #[ tokio:: test]
848940 async fn route_rejects_wrong_host_header ( ) {
849941 let server = spawn_test_server ( ) . await ;
0 commit comments