11import test from 'ava' ;
22import isAbsoluteUrl from './index.js' ;
33
4- test ( 'main' , t => {
4+ test ( 'main - default httpOnly behavior' , t => {
5+ // Allowed protocols with default httpOnly: true
56 t . true ( isAbsoluteUrl ( 'http://sindresorhus.com' ) ) ;
67 t . true ( isAbsoluteUrl ( 'https://sindresorhus.com' ) ) ;
78 t . true ( isAbsoluteUrl ( 'httpS://sindresorhus.com' ) ) ;
8- t . true ( isAbsoluteUrl ( 'file://sindresorhus.com' ) ) ;
9- t . true ( isAbsoluteUrl ( 'mailto:[email protected] ' ) ) ; 10- t . true ( isAbsoluteUrl ( 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D' ) ) ;
9+
10+ // Blocked protocols with default httpOnly: true
11+ t . false ( isAbsoluteUrl ( 'ftp://sindresorhus.com' ) ) ;
12+ t . false ( isAbsoluteUrl ( 'ftps://sindresorhus.com' ) ) ;
13+ t . false ( isAbsoluteUrl ( 'ws://sindresorhus.com' ) ) ;
14+ t . false ( isAbsoluteUrl ( 'wss://sindresorhus.com' ) ) ;
15+ t . false ( isAbsoluteUrl ( 'file://sindresorhus.com' ) ) ;
16+ t . false ( isAbsoluteUrl ( 'mailto:[email protected] ' ) ) ; 17+ t . false ( isAbsoluteUrl ( 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D' ) ) ;
18+ // eslint-disable-next-line no-script-url
19+ t . false ( isAbsoluteUrl ( 'javascript:alert(1)' ) ) ;
20+ t . false ( isAbsoluteUrl ( 'vbscript:alert(1)' ) ) ;
21+
22+ // Not absolute URLs
1123 t . false ( isAbsoluteUrl ( '//sindresorhus.com' ) ) ;
1224 t . false ( isAbsoluteUrl ( '/foo/bar' ) ) ;
1325 t . false ( isAbsoluteUrl ( 'foo/bar' ) ) ;
@@ -17,3 +29,117 @@ test('main', t => {
1729 t . false ( isAbsoluteUrl ( String . raw `C:\Dev\test-broken` ) ) ;
1830 t . false ( isAbsoluteUrl ( 'ht,tp://sindresorhus.com' ) ) ;
1931} ) ;
32+
33+ test ( 'httpOnly: true - explicit' , t => {
34+ // Should be identical to default behavior
35+ t . true ( isAbsoluteUrl ( 'http://example.com' , { httpOnly : true } ) ) ;
36+ t . true ( isAbsoluteUrl ( 'https://example.com' , { httpOnly : true } ) ) ;
37+ t . false ( isAbsoluteUrl ( 'ftp://example.com' , { httpOnly : true } ) ) ;
38+ // eslint-disable-next-line no-script-url
39+ t . false ( isAbsoluteUrl ( 'javascript:alert(1)' , { httpOnly : true } ) ) ;
40+ } ) ;
41+
42+ test ( 'httpOnly: false - allow all absolute URLs' , t => {
43+ // All protocols should be allowed
44+ t . true ( isAbsoluteUrl ( 'http://example.com' , { httpOnly : false } ) ) ;
45+ t . true ( isAbsoluteUrl ( 'https://example.com' , { httpOnly : false } ) ) ;
46+ t . true ( isAbsoluteUrl ( 'ftp://example.com' , { httpOnly : false } ) ) ;
47+ t . true ( isAbsoluteUrl ( 'ws://example.com' , { httpOnly : false } ) ) ;
48+ // eslint-disable-next-line no-script-url
49+ t . true ( isAbsoluteUrl ( 'javascript:alert(1)' , { httpOnly : false } ) ) ;
50+ t . true ( isAbsoluteUrl ( 'vbscript:alert(1)' , { httpOnly : false } ) ) ;
51+ t . true ( isAbsoluteUrl ( 'data:text/html,<h1>XSS</h1>' , { httpOnly : false } ) ) ;
52+ t . true ( isAbsoluteUrl ( 'file:///etc/passwd' , { httpOnly : false } ) ) ;
53+ t . true ( isAbsoluteUrl ( 'mailto:[email protected] ' , { httpOnly :
false } ) ) ; 54+ t . true ( isAbsoluteUrl ( 'tel:+1234567890' , { httpOnly : false } ) ) ;
55+ t . true ( isAbsoluteUrl ( 'custom-scheme://example.com' , { httpOnly : false } ) ) ;
56+ t . true ( isAbsoluteUrl ( 'a:b' , { httpOnly : false } ) ) ;
57+
58+ // Still reject non-absolute URLs
59+ t . false ( isAbsoluteUrl ( '//example.com' , { httpOnly : false } ) ) ;
60+ t . false ( isAbsoluteUrl ( '/path/to/file' , { httpOnly : false } ) ) ;
61+ t . false ( isAbsoluteUrl ( 'relative/path' , { httpOnly : false } ) ) ;
62+ t . false ( isAbsoluteUrl ( 'c:\\' , { httpOnly : false } ) ) ;
63+ } ) ;
64+
65+ test ( 'URLs with ports, authentication, query strings, and fragments' , t => {
66+ // With httpOnly: true (default)
67+ t . true ( isAbsoluteUrl ( 'http://example.com:8080' ) ) ;
68+ t . true ( isAbsoluteUrl ( 'https://example.com:443/path' ) ) ;
69+ t . true ( isAbsoluteUrl ( 'http://user:[email protected] ' ) ) ; 70+ t . true ( isAbsoluteUrl ( 'https://user:[email protected] :8080/path' ) ) ; 71+ t . true ( isAbsoluteUrl ( 'http://example.com?query=value' ) ) ;
72+ t . true ( isAbsoluteUrl ( 'https://example.com#fragment' ) ) ;
73+ t . true ( isAbsoluteUrl ( 'http://example.com/path?query=value#fragment' ) ) ;
74+
75+ // Blocked with httpOnly: true
76+ t . false ( isAbsoluteUrl ( 'ftp://example.com:21' ) ) ;
77+ t . false ( isAbsoluteUrl ( 'ws://example.com:80/socket' ) ) ;
78+ t . false ( isAbsoluteUrl ( 'file://localhost:1234' ) ) ;
79+ t . false ( isAbsoluteUrl ( 'custom://example.com:9999' ) ) ;
80+ } ) ;
81+
82+ test ( 'edge cases' , t => {
83+ // Mixed case protocols with httpOnly: true
84+ t . true ( isAbsoluteUrl ( 'HtTp://example.com' ) ) ;
85+ t . true ( isAbsoluteUrl ( 'HttpS://example.com' ) ) ;
86+
87+ // Previously allowed protocols now blocked for security
88+ t . false ( isAbsoluteUrl ( 'FTps://example.com' ) ) ;
89+ t . false ( isAbsoluteUrl ( 'wSS://example.com' ) ) ;
90+
91+ // Protocol with minimal content
92+ t . true ( isAbsoluteUrl ( 'http:' ) ) ;
93+ t . true ( isAbsoluteUrl ( 'https:' ) ) ;
94+ t . false ( isAbsoluteUrl ( 'ftp:' ) ) ;
95+ t . false ( isAbsoluteUrl ( 'ws:' ) ) ;
96+
97+ // Invalid absolute URLs
98+ t . false ( isAbsoluteUrl ( 'http' ) ) ;
99+ t . false ( isAbsoluteUrl ( 'https' ) ) ;
100+ t . false ( isAbsoluteUrl ( ':http' ) ) ;
101+ t . false ( isAbsoluteUrl ( '://example.com' ) ) ;
102+
103+ // Empty string and whitespace
104+ t . false ( isAbsoluteUrl ( '' ) ) ;
105+ t . false ( isAbsoluteUrl ( ' ' ) ) ;
106+ t . false ( isAbsoluteUrl ( ' http://example.com' ) ) ; // Leading space breaks the pattern
107+ t . true ( isAbsoluteUrl ( 'http://example.com ' ) ) ; // Trailing space doesn't affect absolute URL detection
108+ } ) ;
109+
110+ test ( 'protocol edge cases and security boundaries' , t => {
111+ // Protocol case variations
112+ t . true ( isAbsoluteUrl ( 'Http://example.com' ) ) ;
113+ t . true ( isAbsoluteUrl ( 'HTTPS://example.com' ) ) ;
114+ t . true ( isAbsoluteUrl ( 'hTtP://example.com' ) ) ;
115+
116+ // Protocol boundary testing
117+ t . false ( isAbsoluteUrl ( 'http-custom://example.com' ) ) ; // Should be blocked by httpOnly
118+ t . false ( isAbsoluteUrl ( 'https-modified://example.com' ) ) ; // Should be blocked
119+ t . true ( isAbsoluteUrl ( 'http-custom://example.com' , { httpOnly : false } ) ) ; // Allowed when httpOnly is false
120+
121+ // Potential bypass attempts
122+ t . false ( isAbsoluteUrl ( 'h\u0000ttp://example.com' ) ) ; // Null byte
123+ t . false ( isAbsoluteUrl ( ' http://example.com' ) ) ; // Leading space
124+ t . false ( isAbsoluteUrl ( '\thttp://example.com' ) ) ; // Leading tab
125+ } ) ;
126+
127+ test ( 'unicode and internationalized domain names' , t => {
128+ // Unicode in domain names should work
129+ t . true ( isAbsoluteUrl ( 'http://example.测试' ) ) ;
130+ t . true ( isAbsoluteUrl ( 'https://xn--fsq.xn--0zwm56d' ) ) ; // IDN encoded
131+ t . true ( isAbsoluteUrl ( 'http://münchen.de' ) ) ;
132+
133+ // Unicode in other parts
134+ t . true ( isAbsoluteUrl ( 'http://example.com/测试' ) ) ;
135+ t . true ( isAbsoluteUrl ( 'http://example.com?query=测试' ) ) ;
136+ } ) ;
137+
138+ test ( 'throws on invalid input' , t => {
139+ t . throws ( ( ) => isAbsoluteUrl ( null ) , { instanceOf : TypeError } ) ;
140+ t . throws ( ( ) => isAbsoluteUrl ( undefined ) , { instanceOf : TypeError } ) ;
141+ t . throws ( ( ) => isAbsoluteUrl ( 123 ) , { instanceOf : TypeError } ) ;
142+ t . throws ( ( ) => isAbsoluteUrl ( { } ) , { instanceOf : TypeError } ) ;
143+ t . throws ( ( ) => isAbsoluteUrl ( [ ] ) , { instanceOf : TypeError } ) ;
144+ t . throws ( ( ) => isAbsoluteUrl ( true ) , { instanceOf : TypeError } ) ;
145+ } ) ;
0 commit comments