@@ -29,18 +29,32 @@ class URLSanitizer:
2929 "file" , "gopher" , "data" , "javascript" , "vbscript" ,
3030 "about" , "view-source" , "ws" , "wss"
3131 }
32-
32+
33+ BLOCKED_PORTS : Set [int ] = {
34+ 7 , 9 , 11 , 13 , 15 , 17 , 19 , 20 , 21 , 22 , 23 , 25 , 37 , 42 ,
35+ 43 , 53 , 77 , 79 , 87 , 95 , 101 , 102 , 103 , 104 , 109 , 110 ,
36+ 111 , 113 , 115 , 117 , 119 , 123 , 135 , 139 , 143 , 179 , 389 ,
37+ 443 , 445 , 465 , 512 , 513 , 514 , 515 , 526 , 530 , 531 , 532 ,
38+ 540 , 548 , 554 , 556 , 563 , 587 , 601 , 636 , 993 , 995 ,
39+ 1433 , 1434 , 1521 , 2049 , 2375 , 2376 , 3128 , 3306 , 3389 ,
40+ 4333 , 5432 , 5900 , 5901 , 6000 , 6001 , 6379 , 6666 , 6667 ,
41+ 6668 , 6669 , 7001 , 7002 , 8000 , 8080 , 8081 , 8443 , 9000 ,
42+ 9001 , 9090 , 9200 , 9300 , 11211 , 27017 , 27018 , 27019 ,
43+ }
44+
3345 def __init__ (
3446 self ,
3547 allowed_schemes : Optional [List [str ]] = None ,
3648 allow_localhost : bool = False ,
3749 allow_private : bool = False ,
38- max_length : int = 2048
50+ max_length : int = 2048 ,
51+ allowed_ports : Optional [List [int ]] = None ,
3952 ):
4053 self .allowed_schemes = allowed_schemes or ['http' , 'https' ]
4154 self .allow_localhost = allow_localhost
4255 self .allow_private = allow_private
4356 self .max_length = max_length
57+ self .allowed_ports = allowed_ports
4458 self ._blocked_networks = [ip_network (net ) for net in self .BLOCKED_NETWORKS ]
4559
4660 def validate_url (self , url : str ) -> str :
@@ -79,7 +93,8 @@ def validate_url(self, url: str) -> str:
7993 host = parsed .hostname
8094 if not host :
8195 raise InvalidURLError ("URL must have a valid hostname" )
82-
96+
97+ self ._validate_port (parsed .port )
8398 self ._validate_ip_address (host )
8499 self ._validate_characters (url )
85100
@@ -107,6 +122,21 @@ def _validate_ip_address(self, host: str) -> None:
107122 if not (self .allow_private and ip .is_private ):
108123 raise InvalidURLError (f"Blocked IP address: { host } " )
109124
125+ def _validate_port (self , port : Optional [int ]) -> None :
126+ """Validate port number against allowed/blocked lists."""
127+ if port is None :
128+ return
129+ if port < 1 or port > 65535 :
130+ raise InvalidURLError (f"Invalid port number: { port } " )
131+ if self .allowed_ports is not None :
132+ if port not in self .allowed_ports :
133+ raise InvalidURLError (
134+ f"Port { port } is not allowed. "
135+ f"Allowed ports: { ', ' .join (str (p ) for p in self .allowed_ports )} "
136+ )
137+ elif port in self .BLOCKED_PORTS :
138+ raise InvalidURLError (f"Blocked port: { port } " )
139+
110140 def _validate_characters (self , url : str ) -> None :
111141 """Check for unsafe or malicious characters."""
112142 if '\0 ' in url :
@@ -134,8 +164,13 @@ def is_safe_url(self, url: str) -> bool:
134164def validate_url (
135165 url : str ,
136166 allowed_schemes : Optional [List [str ]] = None ,
167+ allowed_ports : Optional [List [int ]] = None ,
137168 ** kwargs
138169) -> str :
139170 """Convenience function to validate a URL."""
140- sanitizer = URLSanitizer (allowed_schemes = allowed_schemes , ** kwargs )
171+ sanitizer = URLSanitizer (
172+ allowed_schemes = allowed_schemes ,
173+ allowed_ports = allowed_ports ,
174+ ** kwargs
175+ )
141176 return sanitizer .validate_url (url )
0 commit comments