@@ -1850,5 +1850,176 @@ describe("allowed hosts", () => {
18501850
18511851 t . assert . snapshot ( pageErrors ) ;
18521852 } ) ;
1853+
1854+ it ( "should allow websocket connection when host is 'localhost' but resolves to '127.0.0.1' (loopback alias mismatch)" , async ( t ) => {
1855+ const options = {
1856+ allowedHosts : "auto" ,
1857+ host : "localhost" ,
1858+ port : port1 ,
1859+ } ;
1860+
1861+ server = new Server ( options , compiler ) ;
1862+
1863+ await server . start ( ) ;
1864+
1865+ ( { page, browser } = await runBrowser ( ) ) ;
1866+
1867+ page
1868+ . on ( "console" , ( message ) => {
1869+ consoleMessages . push ( message ) ;
1870+ } )
1871+ . on ( "pageerror" , ( error ) => {
1872+ pageErrors . push ( error ) ;
1873+ } ) ;
1874+
1875+ // Simulate: browser opens from localhost, but OS resolved
1876+ // 'localhost' to '127.0.0.1' so host header is the IP
1877+ const headersLocalhostOriginIPv4Host = {
1878+ host : "127.0.0.1" ,
1879+ origin : "http://localhost" ,
1880+ } ;
1881+
1882+ if ( ! server . isSameOrigin ( headersLocalhostOriginIPv4Host ) ) {
1883+ throw new Error (
1884+ "isSameOrigin should treat localhost and 127.0.0.1 as equivalent loopback addresses" ,
1885+ ) ;
1886+ }
1887+
1888+ const response = await page . goto ( `http://localhost:${ port1 } /main.js` , {
1889+ waitUntil : "networkidle0" ,
1890+ } ) ;
1891+
1892+ t . assert . snapshot ( response . status ( ) ) ;
1893+ t . assert . snapshot ( consoleMessages . map ( ( message ) => message . text ( ) ) ) ;
1894+ t . assert . snapshot ( pageErrors ) ;
1895+ } ) ;
1896+
1897+ it ( "should allow websocket connection when host is 'localhost' but resolves to '::1' (loopback alias mismatch)" , async ( t ) => {
1898+ const options = {
1899+ allowedHosts : "auto" ,
1900+ host : "localhost" ,
1901+ port : port1 ,
1902+ } ;
1903+
1904+ server = new Server ( options , compiler ) ;
1905+
1906+ await server . start ( ) ;
1907+
1908+ ( { page, browser } = await runBrowser ( ) ) ;
1909+
1910+ page
1911+ . on ( "console" , ( message ) => {
1912+ consoleMessages . push ( message ) ;
1913+ } )
1914+ . on ( "pageerror" , ( error ) => {
1915+ pageErrors . push ( error ) ;
1916+ } ) ;
1917+
1918+ // Simulate: browser opens from localhost, but OS resolved
1919+ // 'localhost' to '::1' (IPv6) so host header is the IPv6 address
1920+ const headersLocalhostOriginIPv6Host = {
1921+ host : "::1" ,
1922+ origin : "http://localhost" ,
1923+ } ;
1924+
1925+ if ( ! server . isSameOrigin ( headersLocalhostOriginIPv6Host ) ) {
1926+ throw new Error (
1927+ "isSameOrigin should treat localhost and ::1 as equivalent loopback addresses" ,
1928+ ) ;
1929+ }
1930+
1931+ const response = await page . goto ( `http://localhost:${ port1 } /main.js` , {
1932+ waitUntil : "networkidle0" ,
1933+ } ) ;
1934+
1935+ t . assert . snapshot ( response . status ( ) ) ;
1936+ t . assert . snapshot ( consoleMessages . map ( ( message ) => message . text ( ) ) ) ;
1937+ t . assert . snapshot ( pageErrors ) ;
1938+ } ) ;
1939+
1940+ it ( "should allow websocket connection when origin is '127.0.0.1' but host is 'localhost' (reverse loopback alias mismatch)" , async ( t ) => {
1941+ const options = {
1942+ allowedHosts : "auto" ,
1943+ host : "127.0.0.1" ,
1944+ port : port1 ,
1945+ } ;
1946+
1947+ server = new Server ( options , compiler ) ;
1948+
1949+ await server . start ( ) ;
1950+
1951+ ( { page, browser } = await runBrowser ( ) ) ;
1952+
1953+ page
1954+ . on ( "console" , ( message ) => {
1955+ consoleMessages . push ( message ) ;
1956+ } )
1957+ . on ( "pageerror" , ( error ) => {
1958+ pageErrors . push ( error ) ;
1959+ } ) ;
1960+
1961+ // Reverse of above: server bound to 127.0.0.1, but browser
1962+ // sent origin header using 'localhost' name
1963+ const headersIPv4OriginLocalhostHost = {
1964+ host : "localhost" ,
1965+ origin : "http://127.0.0.1" ,
1966+ } ;
1967+
1968+ if ( ! server . isSameOrigin ( headersIPv4OriginLocalhostHost ) ) {
1969+ throw new Error (
1970+ "isSameOrigin should treat 127.0.0.1 and localhost as equivalent loopback addresses" ,
1971+ ) ;
1972+ }
1973+
1974+ const response = await page . goto ( `http://127.0.0.1:${ port1 } /main.js` , {
1975+ waitUntil : "networkidle0" ,
1976+ } ) ;
1977+
1978+ t . assert . snapshot ( response . status ( ) ) ;
1979+ t . assert . snapshot ( consoleMessages . map ( ( message ) => message . text ( ) ) ) ;
1980+ t . assert . snapshot ( pageErrors ) ;
1981+ } ) ;
1982+
1983+ it ( "should NOT allow websocket connection when origin is a non-loopback address mismatching host (loopback fix must not widen trust)" , async ( t ) => {
1984+ const options = {
1985+ allowedHosts : "auto" ,
1986+ host : "localhost" ,
1987+ port : port1 ,
1988+ } ;
1989+
1990+ server = new Server ( options , compiler ) ;
1991+
1992+ await server . start ( ) ;
1993+
1994+ ( { page, browser } = await runBrowser ( ) ) ;
1995+
1996+ page
1997+ . on ( "console" , ( message ) => {
1998+ consoleMessages . push ( message ) ;
1999+ } )
2000+ . on ( "pageerror" , ( error ) => {
2001+ pageErrors . push ( error ) ;
2002+ } ) ;
2003+
2004+ // A real external origin must never pass as loopback equivalent.
2005+ const headersExternalOrigin = {
2006+ host : "localhost" ,
2007+ origin : "http://evil.example.com" ,
2008+ } ;
2009+
2010+ if ( server . isSameOrigin ( headersExternalOrigin ) ) {
2011+ throw new Error (
2012+ "isSameOrigin must NOT allow external origins to match loopback host" ,
2013+ ) ;
2014+ }
2015+
2016+ const response = await page . goto ( `http://localhost:${ port1 } /main.js` , {
2017+ waitUntil : "networkidle0" ,
2018+ } ) ;
2019+
2020+ t . assert . snapshot ( response . status ( ) ) ;
2021+ t . assert . snapshot ( consoleMessages . map ( ( message ) => message . text ( ) ) ) ;
2022+ t . assert . snapshot ( pageErrors ) ;
2023+ } ) ;
18532024 } ) ;
18542025} ) ;
0 commit comments