-
Notifications
You must be signed in to change notification settings - Fork 104
Description
Description:
The progressbar library contains multiple input validation flaws that allow local Denial of Service (DoS) attacks. An attacker can cause memory exhaustion or CPU overload by passing extreme values to constructor parameters.
Tested Environment:
-
OS: Arch Linux (virtual machine)
-
Memory: 3.7GB RAM
Vulnerability Details:
FLAW #1: term_width Parameter Without Validation
def __init__(self, maxval=None, widgets=None, term_width=None, poll=1,
left_justify=True, fd=None):
if term_width is not None:
self.term_width = term_width # NO VALIDATION! Accepts any value
else:
try:
self._handle_resize()
signal.signal(signal.SIGWINCH, self._handle_resize)
self.signal_set = True
except (SystemExit, KeyboardInterrupt): raise
except:
self.term_width = self._env_size() # Can be manipulated via environment variable
....
def _format_line(self):
"""Joins the widgets and justifies the line."""
widgets = ''.join(self._format_widgets())
if self.left_justify:
return widgets.ljust(self.term_width) # CREATES HUGE STRING!
else:
return widgets.rjust(self.term_width) # SAME PROBLEM
Why it happens:
term_width is used directly in ljust()/rjust() without size validation
If term_width = 1,000,000, each update creates a 1MB string
After 100 updates → 100MB allocated → MemoryError and crash
FLAW #2: maxval Parameter Without Validation
def percentage(self):
"""Calculates the percentage of progress."""
if self.maxval is widgets.UnknownLength:
return float("NaN")
if self.currval >= self.maxval:
return 100.0
return (self.currval * 100.0 / self.maxval) if self.maxval else 100.00 # DIVISION WITH HUGE NUMBERS
Why it happens:
maxval can be 10**100 (googol) without validation
Each update calculates currval * 100.0 / maxval
Divisions with astronomical numbers consume excessive CPU
Result: 100% CPU usage, slow system, timeouts
FLAW #3: Iterator Without Iteration Limit
def __next__(self):
try:
value = next(self.__iterable) # ❌ CAN BE INFINITE
if self.start_time is None:
self.start()
else:
self.update(self.currval + 1) # ❌ CALLED FOREVER
return value
except StopIteration:
if self.start_time is None:
self.start()
self.finish()
raise
Why it happens:
__iterable can be an infinite generator (while True: yield)
update() is called on each iteration → cumulative resource consumption
Result: Infinite loop + continuous allocation → memory/CPU crash
FLAW #4: Interval Calculation Without Protection
def start(self):
self.num_intervals = max(100, self.term_width) # CAN BE HUGE
if self.maxval is not widgets.UnknownLength:
if self.maxval < 0: raise ValueError('Value out of range')
self.update_interval = self.maxval / self.num_intervals # DIVISION WITH EXTREME VALUES
Why it happens:
num_intervals = max(100, term_width) → if term_width=10^6, num_intervals=10^6
update_interval = maxval / num_intervals → if maxval=10^100, division with huge numbers
Result: Heavy calculations on each update
FLAW #5: Widgets Can Return Huge Strings
def format_updatable(updatable, pbar):
if hasattr(updatable, 'update'):
return updatable.update(pbar) # ❌ CAN RETURN 1GB STRING
else:
return updatable
Why it happens:
Widgets can implement update() without limits
Return value is concatenated into the final string
Result: Exponential memory allocation
FLAW #6: Environment Can Be Manipulated
def _env_size(self):
"""Tries to find the term_width from the environment."""
return int(os.environ.get('COLUMNS', self._DEFAULT_TERMSIZE)) - 1 # UNTRUSTED ENVIRONMENT VARIABLE
Why it happens:
Attacker can set COLUMNS=1000000 in the environment
term_width will be set to 999999
Result: DoS via environment without passing direct parameters
FLAW #7: Signal Handler Without Protection
def _handle_resize(self, signum=None, frame=None):
"""Tries to catch resize signals sent from the terminal."""
h, w = array('h', ioctl(self.fd, termios.TIOCGWINSZ, '\0' * 8))[:2] # FIXED BUFFER, POSSIBLE OVERFLOW
self.term_width = w # CAN BE HUGE
Why it happens:
ioctl() with '\0'*8 buffer may not be safe
If w is huge (e.g., 65535), term_width becomes gigantic
Result: Next update creates massive string
FLAW #8: Bar Widget With Unlimited Multiplication
def update(self, pbar, width):
# Marked must *always* have length of 1
if pbar.maxval is not UnknownLength and pbar.maxval:
marked *= int(pbar.currval / pbar.maxval * width) # width CAN BE HUGE
else:
marked = ''
Why it happens:
width comes from term_width (can be 1,000,000)
Multiplication marked * int(...) can create huge string
Result: Massive string allocation
CONCLUSION
The DoS vulnerabilities in progressbar occur due to complete lack of input validation on constructor parameters (term_width, maxval) and the absence of limits on operations that scale linearly with these values.