Skip to content

Multiple Local Denial of Service (DoS) vulnerabilities via resource exhaustion #85

@cardosource

Description

@cardosource

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions