VYPR
High severityNVD Advisory· Published Feb 8, 2014· Updated Apr 29, 2026

CVE-2013-2191

CVE-2013-2191

Description

python-bugzilla before 0.9.0 does not validate X.509 certificates, which allows man-in-the-middle attackers to spoof Bugzilla servers via a crafted certificate.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
python-bugzillaPyPI
< 0.9.00.9.0

Affected products

10
  • cpe:2.3:a:python_bugzilla_project:python-bugzilla:*:*:*:*:*:*:*:*+ 4 more
    • cpe:2.3:a:python_bugzilla_project:python-bugzilla:*:*:*:*:*:*:*:*range: <=0.8.0
    • cpe:2.3:a:python_bugzilla_project:python-bugzilla:0.6.0:*:*:*:*:*:*:*
    • cpe:2.3:a:python_bugzilla_project:python-bugzilla:0.6.1:*:*:*:*:*:*:*
    • cpe:2.3:a:python_bugzilla_project:python-bugzilla:0.6.2:*:*:*:*:*:*:*
    • cpe:2.3:a:python_bugzilla_project:python-bugzilla:0.7.0:*:*:*:*:*:*:*
  • cpe:2.3:o:fedoraproject:fedora:17:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:o:fedoraproject:fedora:17:*:*:*:*:*:*:*
    • cpe:2.3:o:fedoraproject:fedora:18:*:*:*:*:*:*:*
  • OpenSUSE/openSUSE3 versions
    cpe:2.3:o:opensuse:opensuse:11.4:*:*:*:*:*:*:*+ 2 more
    • cpe:2.3:o:opensuse:opensuse:11.4:*:*:*:*:*:*:*
    • cpe:2.3:o:opensuse:opensuse:12.2:*:*:*:*:*:*:*
    • cpe:2.3:o:opensuse:opensuse:12.3:*:*:*:*:*:*:*

Patches

1
a782282ee479

CVE-2013-2191: Switch to pycurl to get SSL host and cert validation

5 files changed · +120 55
  • bin/bugzilla+20 5 modified
    @@ -90,6 +90,9 @@ def setup_parser():
         p.add_option('--bztype', default='auto',
                 help="Bugzilla type. Autodetected if not set. "
                      "Available types: %s" % " ".join(bugzilla.classlist))
    +    p.add_option("--nosslverify", dest="sslverify",
    +                 action="store_false", default=True,
    +                 help="Don't error on invalid bugzilla SSL certificate")
         p.add_option('--user',
                 help="username")
         p.add_option('--password',
    @@ -1024,7 +1027,8 @@ def main(bzinstance=None):
             bz = bzinstance
         else:
             bz = bzclass(url=global_opt.bugzilla,
    -                     cookiefile=global_opt.cookiefile or -1)
    +                     cookiefile=global_opt.cookiefile or -1,
    +                     sslverify=global_opt.sslverify)
     
     
         # Handle 'login' action
    @@ -1126,7 +1130,7 @@ if __name__ == '__main__':
             main()
         except KeyboardInterrupt:
             log.debug("", exc_info=True)
    -        print "\ninterrupted."
    +        print "\nExited at user request."
             sys.exit(1)
         except socket.error, e:
             log.debug("", exc_info=True)
    @@ -1139,8 +1143,19 @@ if __name__ == '__main__':
         except xmlrpclib.ProtocolError, e:
             log.debug("", exc_info=True)
             print "\nInvalid server response: %d %s" % (e.errcode, e.errmsg)
    -        redir = e.headers.getheader("location", 0)
    +
    +        # Give SSL recommendations
    +        import pycurl
    +        sslerrcodes = [getattr(pycurl, ename) for ename in dir(pycurl) if
    +                       ename.startswith("E_SSL")]
    +        if e.errcode in sslerrcodes:
    +            print ("\nIf you trust the remote server, you can work "
    +                   "around this error with:\n"
    +                  "  bugzilla --nosslverify ...")
    +
    +        # Detect redirect
    +        redir = (e.headers and e.headers.getheader("location", 0) or None)
             if redir:
    -            print "Server was attempting a redirect."
    -            print 'Try "bugzilla --bugzilla %s ..."' % redir
    +            print ("\nServer was attempting a redirect. Try: "
    +                   "  bugzilla --bugzilla %s ..." % redir)
             sys.exit(4)
    
  • bugzilla/base.py+94 45 modified
    @@ -11,9 +11,13 @@
     
     import cookielib
     import os
    +import StringIO
     import urllib2
    +import urlparse
     import xmlrpclib
     
    +import pycurl
    +
     from bugzilla import __version__, log
     from bugzilla.bug import _Bug, _User
     
    @@ -95,43 +99,73 @@ def _build_cookiejar(cookiefile):
         return retcj
     
     
    -# CookieTransport code mostly borrowed from pybugz
    -class _CookieTransport(xmlrpclib.Transport):
    -    def __init__(self, uri, cookiejar, use_datetime=0):
    -        self.verbose = 0
    -
    -        # python 2.4 compat
    +class _CURLTransport(xmlrpclib.Transport):
    +    def __init__(self, url, cookiejar,
    +                 sslverify=True, sslcafile=None, debug=0):
             if hasattr(xmlrpclib.Transport, "__init__"):
    -            xmlrpclib.Transport.__init__(self, use_datetime=use_datetime)
    +            xmlrpclib.Transport.__init__(self, use_datetime=False)
    +
    +        self.verbose = debug
    +
    +        # transport constructor needs full url too, as xmlrpc does not pass
    +        # scheme to request
    +        self.scheme = urlparse.urlparse(url)[0]
    +        if self.scheme not in ["http", "https"]:
    +            raise Exception("Invalid URL scheme: %s (%s)" % (self.scheme, url))
    +
    +        self.c = pycurl.Curl()
    +        self.c.setopt(pycurl.POST, 1)
    +        self.c.setopt(pycurl.CONNECTTIMEOUT, 30)
    +        self.c.setopt(pycurl.HTTPHEADER, [
    +            "Content-Type: text/xml",
    +        ])
    +        self.c.setopt(pycurl.VERBOSE, debug)
    +
    +        self.set_cookiejar(cookiejar)
    +
    +        # ssl settings
    +        if self.scheme == "https":
    +            # override curl built-in ca file setting
    +            if sslcafile is not None:
    +                self.c.setopt(pycurl.CAINFO, sslcafile)
    +
    +            # disable ssl verification
    +            if not sslverify:
    +                self.c.setopt(pycurl.SSL_VERIFYPEER, 0)
    +                self.c.setopt(pycurl.SSL_VERIFYHOST, 0)
    +
    +    def set_cookiejar(self, cj):
    +        self.c.setopt(pycurl.COOKIEFILE, cj.filename or "")
    +        self.c.setopt(pycurl.COOKIEJAR, cj.filename or "")
    +
    +    def get_cookies(self):
    +        return self.c.getinfo(pycurl.INFO_COOKIELIST)
    +
    +    def open_helper(self, url, request_body):
    +        self.c.setopt(pycurl.URL, url)
    +        self.c.setopt(pycurl.POSTFIELDS, request_body)
    +
    +        b = StringIO.StringIO()
    +        self.c.setopt(pycurl.WRITEFUNCTION, b.write)
    +        try:
    +            self.c.perform()
    +        except pycurl.error, e:
    +            raise xmlrpclib.ProtocolError(url, e[0], e[1], None)
     
    -        self.uri = uri
    -        self.opener = urllib2.build_opener()
    -        self.opener.add_handler(urllib2.HTTPCookieProcessor(cookiejar))
    +        b.seek(0)
    +        return b
     
         def request(self, host, handler, request_body, verbose=0):
    -        req = urllib2.Request(self.uri)
    -        req.add_header('User-Agent', self.user_agent)
    -        req.add_header('Content-Type', 'text/xml')
    +        self.verbose = verbose
    +        url = "%s://%s%s" % (self.scheme, host, handler)
     
    -        if hasattr(self, 'accept_gzip_encoding') and self.accept_gzip_encoding:
    -            req.add_header('Accept-Encoding', 'gzip')
    +        # xmlrpclib fails to escape \r
    +        request_body = request_body.replace('\r', '&#xd;')
     
    -        req.add_data(request_body)
    +        stringio = self.open_helper(url, request_body)
    +        return self.parse_response(stringio)
     
    -        resp = self.opener.open(req)
     
    -        # In Python 2, resp is a urllib.addinfourl instance, which does not
    -        # have the getheader method that parse_response expects.
    -        if not hasattr(resp, 'getheader'):
    -            resp.getheader = resp.headers.getheader
    -
    -        if resp.code == 200:
    -            self.verbose = verbose
    -            return self.parse_response(resp)
    -
    -        resp.close()
    -        raise xmlrpclib.ProtocolError(self.uri, resp.status,
    -                                      resp.reason, resp.msg)
     
     
     class BugzillaError(Exception):
    @@ -186,8 +220,6 @@ def url_to_query(url):
             Given a big huge bugzilla query URL, returns a query dict that can
             be passed along to the Bugzilla.query() method.
             '''
    -        import urlparse
    -
             q = {}
             (ignore, ignore, path,
              ignore, query, ignore) = urlparse.urlparse(url)
    @@ -219,13 +251,16 @@ def fix_url(url):
                 url = url + '/xmlrpc.cgi'
             return url
     
    -    def __init__(self, url=None, user=None, password=None, cookiefile=-1):
    +    def __init__(self, url=None, user=None, password=None, cookiefile=-1,
    +                 sslverify=True):
             # Settings the user might want to tweak
             self.user = user or ''
             self.password = password or ''
             self.url = ''
     
    +        self._transport = None
             self._cookiejar = None
    +        self._sslverify = bool(sslverify)
     
             self.logged_in = False
     
    @@ -371,9 +406,11 @@ def connect(self, url=None):
                 url = self.url
             url = self.fix_url(url)
     
    -        transport = _CookieTransport(url, self._cookiejar)
    -        transport.user_agent = self.user_agent
    -        self._proxy = xmlrpclib.ServerProxy(url, transport)
    +        self._transport = _CURLTransport(url, self._cookiejar,
    +                                         sslverify=self._sslverify)
    +        self._transport.user_agent = self.user_agent
    +        self._proxy = xmlrpclib.ServerProxy(url, self._transport)
    +
     
             self.url = url
             # we've changed URLs - reload config
    @@ -431,8 +468,6 @@ def login(self, user=None, password=None):
             except xmlrpclib.Fault:
                 r = False
     
    -        if r and self._cookiejar.filename is not None:
    -            self._cookiejar.save()
             return r
     
         def logout(self):
    @@ -1178,18 +1213,32 @@ def openattachment(self, attachid):
             '''Get the contents of the attachment with the given attachment ID.
             Returns a file-like object.'''
             att_uri = self._attachment_uri(attachid)
    -        opener = urllib2.build_opener(
    -            urllib2.HTTPCookieProcessor(self._cookiejar))
    -        att = opener.open(att_uri)
     
    -        # RFC 2183 defines the content-disposition header, if you're curious
    -        disp = att.headers['content-disposition'].split(';')
    +        headers = {}
    +        ret = StringIO.StringIO()
    +
    +        def headers_cb(buf):
    +            if not ":" in buf:
    +                return
    +            name, val = buf.split(":", 1)
    +            headers[name.lower()] = val
    +
    +        c = pycurl.Curl()
    +        c.setopt(pycurl.URL, att_uri)
    +        c.setopt(pycurl.WRITEFUNCTION, ret.write)
    +        c.setopt(pycurl.HEADERFUNCTION, headers_cb)
    +        c.setopt(pycurl.COOKIEFILE, self._cookiejar.filename or "")
    +        c.perform()
    +        c.close()
    +
    +        disp = headers['content-disposition'].split(';')
             disp.pop(0)
             parms = dict([p.strip().split("=", 1) for p in disp])
    -        # Parameter values can be quoted/encoded as per RFC 2231
    -        att.name = _decode_rfc2231_value(parms['filename'])
    +        ret.name = _decode_rfc2231_value(parms['filename'])
    +
             # Hooray, now we have a file-like object with .read() and .name
    -        return att
    +        ret.seek(0)
    +        return ret
     
         def updateattachmentflags(self, bugid, attachid, flagname, **kwargs):
             '''
    
  • bugzilla/__init__.py+1 1 modified
    @@ -97,7 +97,7 @@ class Bugzilla(object):
         def __init__(self, **kwargs):
             log.info("Bugzilla v%s initializing" % __version__)
             if 'url' not in kwargs:
    -            raise TypeError("You must pass a valid bugzilla xmlrpc.cgi URL")
    +            raise TypeError("You must pass a valid bugzilla URL")
     
             # pylint: disable=W0233
             # Use of __init__ of non parent class
    
  • python-bugzilla.spec+1 0 modified
    @@ -19,6 +19,7 @@ BuildRequires:  python-setuptools
     BuildRequires:  python-setuptools-devel
     %endif
     
    +Requires: python-pycurl
     Requires: python-magic
     
     %description
    
  • tests/ro_functional.py+4 4 modified
    @@ -74,13 +74,13 @@ def _testQuery(self, args, mincount, expectbug):
                 return
     
             self.assertTrue(len(out.splitlines()) >= mincount)
    -        self.assertTrue(any([l.startswith("#" + expectbug)
    -                             for l in out.splitlines()]))
    +        self.assertTrue(bool([l for l in out.splitlines() if
    +                              l.startswith("#" + expectbug)]))
     
             # Check --ids output option
             out2 = self.clicomm(cli + " --ids")
             self.assertTrue(len(out.splitlines()) == len(out2.splitlines()))
    -        self.assertTrue(any([l == expectbug for l in out2.splitlines()]))
    +        self.assertTrue(bool([l for l in out2.splitlines() if l == expectbug]))
     
     
         def _testQueryFull(self, bugid, mincount, expectstr):
    @@ -145,7 +145,7 @@ class BZ34(BaseTest):
     
     
     class BZ42(BaseTest):
    -    url = "https://bugzilla.freedesktop.org/xmlrpc.cgi"
    +    url = "https://bugs.freedesktop.org/xmlrpc.cgi"
         bzclass = bugzilla.Bugzilla4
         closestatus = "CLOSED,RESOLVED"
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

10

News mentions

0

No linked articles in our index yet.