1313import csv
1414import sys
1515import urllib3
16+ import re
17+ import ipaddress
18+ import os
1619from typing import Dict , List , Optional , Union , Tuple , Any
1720from concurrent .futures import ThreadPoolExecutor , as_completed
1821from cryptography import x509
3639DEFAULT_HTTP_TIMEOUT : int = 5
3740DEFAULT_HTTPS_TIMEOUT : int = 5
3841
42+ # Security-related constants
43+ VALID_HOSTNAME_REGEX = re .compile (r'^[a-zA-Z0-9\-_\.]+$' )
44+ VALID_IP_REGEX = re .compile (r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' )
45+ VALID_PORT_RANGE = range (1 , 65536 )
46+ VALID_FILE_PATH_MAX_LENGTH = 4096
47+ MAX_PORT_RANGES = 50
48+
49+ # Input validation and sanitization functions
50+ def validate_hostname (hostname : str ) -> bool :
51+ """Validate hostname for security"""
52+ if not hostname or len (hostname ) > 253 : # RFC 1035 limit
53+ return False
54+
55+ # Check for valid hostname format
56+ if not VALID_HOSTNAME_REGEX .match (hostname ):
57+ return False
58+
59+ # Prevent localhost/private addresses
60+ private_hosts = ['localhost' , '127.0.0.1' , '::1' ]
61+ if hostname .lower () in private_hosts :
62+ return False
63+
64+ # Try to validate as IP if it looks like one
65+ if VALID_IP_REGEX .match (hostname ):
66+ try :
67+ ipaddress .ip_address (hostname )
68+ except ValueError :
69+ return False
70+
71+ # Additional validation for hostname format
72+ try :
73+ # Check if the hostname can be encoded properly
74+ hostname .encode ('idna' ).decode ('utf-8' )
75+ return True
76+ except (UnicodeError , UnicodeDecodeError ):
77+ return False
78+
79+ def validate_port (port : int ) -> bool :
80+ """Validate port number for security"""
81+ return port in VALID_PORT_RANGE
82+
83+ def validate_ports_list (ports_string : str ) -> Tuple [bool , List [int ]]:
84+ """Validate and parse comma-separated port ranges"""
85+ if len (ports_string ) > 1000 : # Prevent DoS with large inputs
86+ return False , []
87+
88+ ports = []
89+ seen_ports = set ()
90+
91+ try :
92+ parts = ports_string .split (',' )
93+ if len (parts ) > MAX_PORT_RANGES :
94+ return False , []
95+
96+ for part in parts :
97+ part = part .strip ()
98+ if '-' in part :
99+ try :
100+ start_str , end_str = part .split ('-' )
101+ start , end = int (start_str ), int (end_str )
102+ if not all (validate_port (x ) for x in [start , end ]):
103+ return False , []
104+ if start > end or (end - start ) > 1000 : # Prevent large ranges
105+ return False , []
106+ for p in range (start , end + 1 ):
107+ if p not in seen_ports :
108+ ports .append (p )
109+ seen_ports .add (p )
110+ except (ValueError , IndexError ):
111+ return False , []
112+ else :
113+ try :
114+ port = int (part )
115+ if not validate_port (port ) or port in seen_ports :
116+ return False , []
117+ ports .append (port )
118+ seen_ports .add (port )
119+ except ValueError :
120+ return False , []
121+
122+ except Exception :
123+ return False , []
124+
125+ return True , ports
126+
127+ def sanitize_file_path (file_path : str ) -> Optional [str ]:
128+ """Sanitize file path to prevent directory traversal"""
129+ if not file_path or len (file_path ) > VALID_FILE_PATH_MAX_LENGTH :
130+ return None
131+
132+ # Expand path and resolve any symbolic links
133+ try :
134+ expanded = os .path .expanduser (file_path )
135+ resolved = os .path .abspath (expanded )
136+ # Check if path is still within acceptable bounds
137+ if '..' in resolved or not resolved .startswith (os .getcwd () if not os .path .isabs (expanded ) else '/' ):
138+ return None
139+ return resolved
140+ except (OSError , ValueError ):
141+ return None
142+
143+ def secure_headers_check (url : str , verify_ssl : bool = True ) -> Tuple [Optional [requests .structures .CaseInsensitiveDict ], Optional [int ], Optional [List [requests .Response ]]]:
144+ """Secure version of HTTP headers check with SSL verification"""
145+ try :
146+ headers = {
147+ 'User-Agent' : (
148+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
149+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
150+ 'Chrome/91.0.4472.124 Safari/537.36'
151+ )
152+ }
153+ response = requests .head (
154+ url ,
155+ headers = headers ,
156+ timeout = DEFAULT_HTTP_TIMEOUT ,
157+ verify = verify_ssl ,
158+ allow_redirects = True
159+ )
160+ return response .headers , response .status_code , response .history
161+ except requests .RequestException as e :
162+ logging .warning (f"Error checking { url } : HTTP request failed ({ 'SSL verification' if 'certificate verify failed' in str (e ) else 'connection error' } )" )
163+ return None , None , None
164+
165+ # Advanced rate limiting class
166+ class AdvancedRateLimiter :
167+ def __init__ (self , max_requests : int = 5 , time_window : float = 1.0 ):
168+ self .max_requests = max_requests
169+ self .time_window = time_window
170+ self .requests = []
171+ self .lock = threading .Lock ()
172+
173+ def acquire (self ) -> bool :
174+ """Acquire permission to make a request"""
175+ with self .lock :
176+ now = time .time ()
177+ # Remove requests outside the time window
178+ self .requests = [req for req in self .requests if now - req < self .time_window ]
179+
180+ if len (self .requests ) < self .max_requests :
181+ self .requests .append (now )
182+ return True
183+ return False
184+
185+ def __enter__ (self ):
186+ # Simple wait-based acquisition
187+ while not self .acquire ():
188+ time .sleep (0.1 )
189+ return self
190+
191+ def __exit__ (self , exc_type , exc_val , exc_tb ):
192+ pass
193+
39194# Load indicators from external files
40195def load_indicators (file_path : str ) -> List [str ]:
41196 try :
@@ -190,7 +345,7 @@ def get_geoip_info(ip: str) -> Optional[Dict[str, Union[str, float, None]]]:
190345 return None
191346
192347# Main detection function
193- async def detect_proxy (host : str , common_ports : List [int ], proxy_indicators : List [str ], waf_indicators : Dict [str , str ]) -> Dict [str , Union [str , None , Dict [str , Any ], List [Any ]]]:
348+ async def detect_proxy (host : str , common_ports : List [int ], proxy_indicators : List [str ], waf_indicators : Dict [str , str ], verify_ssl : bool = False ) -> Dict [str , Union [str , None , Dict [str , Any ], List [Any ]]]:
194349 results = {
195350 'host' : host ,
196351 'ip' : None ,
@@ -256,8 +411,8 @@ async def detect_proxy(host: str, common_ports: List[int], proxy_indicators: Lis
256411 http_url = f"http://{ host } "
257412 https_url = f"https://{ host } "
258413
259- http_headers , http_status , http_history = check_http_headers (http_url )
260- https_headers , https_status , https_history = check_http_headers (https_url )
414+ http_headers , http_status , http_history = secure_headers_check (http_url , verify_ssl )
415+ https_headers , https_status , https_history = secure_headers_check (https_url , verify_ssl )
261416
262417 if http_headers :
263418 results ['http_headers' ] = dict (http_headers )
@@ -344,13 +499,50 @@ def main():
344499 parser .add_argument ("-f" , "--file" , help = "Output file path" )
345500 parser .add_argument ("-l" , "--log-level" , choices = ['DEBUG' , 'INFO' , 'WARNING' , 'ERROR' ], default = 'INFO' , help = "Set the logging level (default: INFO)" )
346501 parser .add_argument ("-v" , "--verbose" , action = "store_true" , help = "Enable verbose output (equivalent to --log-level DEBUG)" )
502+ parser .add_argument ("--verify-ssl" , action = "store_true" , help = "Enable SSL certificate verification (default: disabled)" )
503+ parser .add_argument ("--rate-limit" , type = int , default = 5 , help = "Maximum concurrent connections (default: 5)" )
504+ parser .add_argument ("--rate-window" , type = float , default = 1.0 , help = "Rate limiting time window in seconds (default: 1.0)" )
347505 args = parser .parse_args ()
348506
349507 if args .verbose :
350508 logging .getLogger ().setLevel (logging .DEBUG )
351509 else :
352510 logging .getLogger ().setLevel (getattr (logging , args .log_level ))
353511
512+ # Validate inputs for security
513+ if args .target :
514+ if not validate_hostname (args .target ):
515+ logging .error (f"Invalid hostname or IP address: { args .target } " )
516+ return
517+
518+ if args .target_file :
519+ sanitized_path = sanitize_file_path (args .target_file )
520+ if not sanitized_path :
521+ logging .error (f"Invalid file path: { args .target_file } " )
522+ return
523+ args .target_file = sanitized_path
524+
525+ # Validate ports if provided
526+ if args .ports :
527+ valid , port_list = validate_ports_list (args .ports )
528+ if not valid :
529+ logging .error (f"Invalid port specification: { args .ports } " )
530+ return
531+ common_ports = port_list
532+ else :
533+ common_ports = [80 , 443 , 8080 , 3128 , 8443 , 8888 , 8880 , 8000 , 9000 , 9090 ]
534+
535+ # Set up global rate limiting
536+ if args .rate_limit < 1 or args .rate_limit > 100 :
537+ logging .error ("Rate limit must be between 1 and 100" )
538+ return
539+
540+ global rate_limit
541+ rate_limit = AdvancedRateLimiter (
542+ max_requests = args .rate_limit ,
543+ time_window = args .rate_window
544+ )
545+
354546 # Load indicators
355547 proxy_indicators = load_indicators ('proxy_indicators.txt' ) or [
356548 # Default proxy indicators if file is not found
@@ -364,18 +556,7 @@ def main():
364556 # ... (other indicators as in previous examples)
365557 }
366558
367- # Determine ports to scan
368- if args .ports :
369- ports = []
370- for part in args .ports .split (',' ):
371- if '-' in part :
372- start , end = map (int , part .split ('-' ))
373- ports .extend (range (start , end + 1 ))
374- else :
375- ports .append (int (part ))
376- common_ports = ports
377- else :
378- common_ports = [80 , 443 , 8080 , 3128 , 8443 , 8888 , 8880 , 8000 , 9000 , 9090 ]
559+
379560
380561 # Determine targets to scan
381562 if args .target_file :
@@ -395,7 +576,7 @@ def main():
395576 for target in targets :
396577 loop = asyncio .get_event_loop ()
397578 results = loop .run_until_complete (
398- detect_proxy (target , common_ports , proxy_indicators , waf_indicators )
579+ detect_proxy (target , common_ports , proxy_indicators , waf_indicators , args . verify_ssl )
399580 )
400581 all_results .append (results )
401582
0 commit comments