CVE-2019-7539 (Python)

A code injection issue was discovered in ipycache through 2016-05-31.

Weakness:
CWE-94: name: Improper Control of Generation of Code ('Code Injection')
description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CWE-77: name: Improper Neutralization of Special Elements used in a Command ('Command Injection')
description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
CWE-78: name: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

Repository: https://github.com/adi928/ipycache

Vulnerable code:
file_path: ipycache.py
snippet: def load_vars(path, vars):
    """Load variables from a pickle file.
    
    Arguments:
    
      * path: the path to the pickle file.
      * vars: a list of variable names.
    
    Returns:
    
      * cache: a dictionary {var_name: var_value}.
    
    """
    with open(path, 'rb') as f:
        # Load the variables from the cache.
        try:
            cache = pickle.load(f)
        except EOFError as e:
            cache={}
            #raise IOError(str(e))
        
        # Check that all requested variables could be loaded successfully
        # from the cache.
        missing_vars = sorted(set(vars) - set(cache.keys()))
        if missing_vars:
            raise ValueError(("The following variables could not be loaded "
                "from the cache: {0:s}").format(
                ', '.join(["'{0:s}'".format(var) for var in missing_vars])))
        additional_vars = sorted(set(cache.keys()) - set(vars))
        for hidden_variable in '_captured_io', '_cell_md5':
            try:
                additional_vars.remove(hidden_variable)
            except ValueError:
                pass
        if additional_vars:
            raise ValueError("The following variables were present in the cache, "
                    "but removed from the storage request: {0:s}".format(
                ', '.join(["'{0:s}'".format(var) for var in additional_vars])))
        
        return cache

Fixed code:
file_path: ipycache.py
snippet: def load_vars(path, vars):
    """Load variables from a pickle file.
    
    Arguments:
    
      * path: the path to the pickle file.
      * vars: a list of variable names.
    
    Returns:
    
      * cache: a dictionary {var_name: var_value}.
    
    """
    with open(path, 'rb') as f:
        # Load the variables from the cache.
        try:
            restricted_loads(f.read())
            cache = pickle.load(f)
        except EOFError as e:
            cache={}
            #raise IOError(str(e))
        
        # Check that all requested variables could be loaded successfully
        # from the cache.
        missing_vars = sorted(set(vars) - set(cache.keys()))
        if missing_vars:
            raise ValueError(("The following variables could not be loaded "
                "from the cache: {0:s}").format(
                ', '.join(["'{0:s}'".format(var) for var in missing_vars])))
        additional_vars = sorted(set(cache.keys()) - set(vars))
        for hidden_variable in '_captured_io', '_cell_md5':
            try:
                additional_vars.remove(hidden_variable)
            except ValueError:
                pass
        if additional_vars:
            raise ValueError("The following variables were present in the cache, "
                    "but removed from the storage request: {0:s}".format(
                ', '.join(["'{0:s}'".format(var) for var in additional_vars])))
        
        return cache


file_path: ipycache.py
snippet: class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        if module == '_io' and name == 'StringIO':
            return getattr(sys.modules[module], name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))


def restricted_loads(s):
    """Helper function analogous to pickle.loads()."""
    return RestrictedUnpickler(io.BytesIO(s)).load()