@@ -159,3 +159,65 @@ test('parse canonicalises IDN / Unicode hosts to their ASCII form', (t) => {
159159 t . equal ( parsed . host , expectedHost , `host canonicalised to ASCII: ${ description } ` )
160160 } )
161161} )
162+
163+ test ( 'parse rejects a literal backslash in the authority as malformed (RFC 3986)' , ( t ) => {
164+ // Regression for the host-confusion bypass: a literal "\" is invalid RFC 3986
165+ // syntax and must be flagged malformed, not silently rewritten. Otherwise "\"
166+ // acts as a host delimiter here while Node's native URL parses a different
167+ // host, defeating a host-based SSRF/redirect/origin allowlist.
168+ const cases = [
169+ 'http://evil.com\\@allowed.com' ,
170+ 'https://169.254.169.254\\@trusted.example.com' ,
171+ 'http://127.0.0.1\\@public.example.com' ,
172+ 'https://attacker.com\\@api.internal' ,
173+ 'http://a\\@b' ,
174+ 'ws://evil.com\\@allowed.com/chat' ,
175+ 'wss://evil.com\\@allowed.com/chat' ,
176+ 'http://evil.com\\%40allowed.com' ,
177+ '//evil.com\\@allowed.com'
178+ ]
179+
180+ t . plan ( cases . length )
181+
182+ cases . forEach ( ( input ) => {
183+ t . equal (
184+ fastURI . parse ( input ) . error ,
185+ 'URI authority must not contain a literal backslash.' ,
186+ input
187+ )
188+ } )
189+ } )
190+
191+ test ( 'normalize does not canonicalize a literal-backslash URI into a different valid URL' , ( t ) => {
192+ const cases = [
193+ 'http://evil.com\\@allowed.com' ,
194+ 'https://attacker.com\\@api.internal'
195+ ]
196+
197+ t . plan ( cases . length )
198+
199+ cases . forEach ( ( input ) => {
200+ t . equal ( fastURI . normalize ( input ) , input , input )
201+ } )
202+ } )
203+
204+ test ( 'parse leaves percent-encoded %5C untouched as encoded data (not rejected)' , ( t ) => {
205+ // Only the literal "\" byte is rejected; %5C stays valid encoded data and
206+ // does not diverge from the native URL parser, so it must not be flagged.
207+ const input = 'http://evil.com%5C@allowed.com'
208+ const parsed = fastURI . parse ( input )
209+
210+ t . plan ( 2 )
211+ t . notOk ( parsed . error , '%5C is valid encoded data, not malformed' )
212+ t . equal ( parsed . host , new URL ( input ) . hostname , '%5C host matches native URL (no divergence)' )
213+ } )
214+
215+ test ( 'parse does not reject a literal backslash in the query or fragment' , ( t ) => {
216+ // The rejection is scoped to the authority/path (the host-confusion surface);
217+ // a backslash after "?"/"#" is normalized as encoded data as before.
218+ const parsed = fastURI . parse ( 'http://host.example.com/?x=\\y#z\\w' )
219+
220+ t . plan ( 2 )
221+ t . notOk ( parsed . error , 'backslash in query/fragment does not mark the URI malformed' )
222+ t . equal ( parsed . host , 'host.example.com' , 'host parsed normally' )
223+ } )
0 commit comments