diff --git a/.gitignore b/.gitignore index 66dbf51..a902e2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ *.egg-info +MANIFEST.in +build/ +dist/ +setup.cfg +venv/ *.pyc diff --git a/CHANGES.txt b/CHANGES.txt index 6290124..ec8627e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -3,6 +3,8 @@ CHANGELOG 0.12.6 (unreleased) ----------------------- +- fix cve CVE Request ---- SOAPpy 0.12.5 Multiple Vulnerabilities -- XXE part + [kiorky] - Remove dependency on fpconst. - adding maptype [Sandro Knauß] - Support / (and other reserved characters) in the password. [Ionut Turturica] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..adeb0dc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +include *.txt *.cfg *.rst +recursive-include validate * +recursive-include contrib * +recursive-include src * +recursive-include tests * +recursive-include tools * +recursive-include zope * +recursive-include docs * +recursive-include bid * +global-exclude *pyc diff --git a/src/SOAPpy/Parser.py b/src/SOAPpy/Parser.py index a3127f2..980555c 100644 --- a/src/SOAPpy/Parser.py +++ b/src/SOAPpy/Parser.py @@ -1,4 +1,5 @@ # SOAPpy modules +import traceback from Config import Config from Types import * from NS import NS @@ -7,6 +8,10 @@ from Utilities import * import string import xml.sax from wstools.XMLname import fromXMLname +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO try: from M2Crypto import SSL except: pass @@ -93,7 +98,7 @@ class SOAPParser(xml.sax.handler.ContentHandler): elif prefix: tag = prefix + ":" + tag return tag - + # Workaround two sax bugs if name[0] == None and name[1][0] == ' ': name = (None, name[1][1:]) @@ -127,7 +132,7 @@ class SOAPParser(xml.sax.handler.ContentHandler): elif self._next == "": raise Error, "expected nothing, " \ "got `%s'" % toStr( name ) - + if len(self._stack) == 2: rules = self._rules @@ -275,7 +280,7 @@ class SOAPParser(xml.sax.handler.ContentHandler): null = 1 # check for nil=1, but watch out for string values - try: + try: null = int(null) except ValueError, e: if not e[0].startswith("invalid literal for int()"): @@ -312,7 +317,7 @@ class SOAPParser(xml.sax.handler.ContentHandler): #print "cur.kind=", cur.kind #print "cur.rules=", cur.rules #print "\n" - + if cur.rules != None: rule = cur.rules @@ -374,7 +379,7 @@ class SOAPParser(xml.sax.handler.ContentHandler): # print "ns:", ns # print "attrs:", attrs # print "kind:", kind - + if kind == None: # If the current item's container is an array, it will @@ -863,7 +868,7 @@ class SOAPParser(xml.sax.handler.ContentHandler): # print " attrs=", attrs # print " t[0]=", t[0] # print " t[1]=", t[1] - + # print " in?", t[0] in NS.EXSD_L if t[0] in NS.EXSD_L: @@ -933,11 +938,11 @@ class SOAPParser(xml.sax.handler.ContentHandler): elif d == 0: if type(self.zerofloatre) == StringType: self.zerofloatre = re.compile(self.zerofloatre) - + if self.zerofloatre.search(s): raise UnderflowError, "invalid %s: %s" % (t[1], s) return d - + if t[1] in ("dateTime", "date", "timeInstant", "time"): return self.convertDateTime(d, t[1]) if t[1] == "decimal": @@ -1031,14 +1036,17 @@ class SOAPParser(xml.sax.handler.ContentHandler): ################################################################################ # call to SOAPParser that keeps all of the info ################################################################################ -def _parseSOAP(xml_str, rules = None): - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO +class EmptyEntityResolver(xml.sax.handler.EntityResolver): + def resolveEntity(self, publicId, systemId): + return StringIO("") + + +def _parseSOAP(xml_str, rules = None, ignore_ext=None): + if ignore_ext is None: + ignore_ext = False parser = xml.sax.make_parser() - t = SOAPParser(rules = rules) + t = SOAPParser(rules=rules) parser.setContentHandler(t) e = xml.sax.handler.ErrorHandler() parser.setErrorHandler(e) @@ -1046,15 +1054,19 @@ def _parseSOAP(xml_str, rules = None): inpsrc = xml.sax.xmlreader.InputSource() inpsrc.setByteStream(StringIO(xml_str)) + # disable by default entity loading on posted content + if ignore_ext: + parser.setEntityResolver(EmptyEntityResolver()) # turn on namespace mangeling - parser.setFeature(xml.sax.handler.feature_namespaces,1) + parser.setFeature(xml.sax.handler.feature_namespaces, 1) try: parser.parse(inpsrc) except xml.sax.SAXParseException, e: parser._parser = None + print traceback.format_exc() raise e - + return t ################################################################################ @@ -1068,9 +1080,9 @@ def parseSOAP(xml_str, attrs = 0): return t.body -def parseSOAPRPC(xml_str, header = 0, body = 0, attrs = 0, rules = None): +def parseSOAPRPC(xml_str, header = 0, body = 0, attrs = 0, rules = None, ignore_ext=None): - t = _parseSOAP(xml_str, rules = rules) + t = _parseSOAP(xml_str, rules = rules, ignore_ext=ignore_ext) p = t.body[0] # Empty string, for RPC this translates into a void @@ -1080,7 +1092,7 @@ def parseSOAPRPC(xml_str, header = 0, body = 0, attrs = 0, rules = None): if k[0] != "_": name = k p = structType(name) - + if header or body or attrs: ret = (p,) if header : ret += (t.header,) diff --git a/src/SOAPpy/Server.py b/src/SOAPpy/Server.py index 0a3befe..b2c2c2c 100644 --- a/src/SOAPpy/Server.py +++ b/src/SOAPpy/Server.py @@ -188,8 +188,9 @@ class SOAPServerBase: if namespace[0] == ":": namespace = namespace[1:] del self.objmap[namespace] - + class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + ignore_ext = True def version_string(self): return '' + \ 'SOAPpy ' + __version__ + ' (Python ' + \ @@ -204,7 +205,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_POST(self): global _contexts - + status = 500 try: if self.server.config.dumpHeadersIn: @@ -226,7 +227,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): debugFooter(s) (r, header, body, attrs) = \ - parseSOAPRPC(data, header = 1, body = 1, attrs = 1) + parseSOAPRPC(data, header = 1, body = 1, attrs = 1, ignore_ext=self.ignore_ext) method = r._name args = r._aslist() @@ -252,8 +253,8 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): ordered_args = {} named_args = {} - if Config.specialArgs: - + if Config.specialArgs: + for (k,v) in kw.items(): if k[0]=="v": @@ -271,13 +272,13 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # if r._ns is specified use it, if not check for # a path, if it's specified convert it and use it as the # namespace. If both are specified, use r._ns. - + ns = r._ns if len(self.path) > 1 and not ns: ns = self.path.replace("/", ":") if ns[0] == ":": ns = ns[1:] - + # authorization method a = None @@ -291,9 +292,9 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): #print '<-> Argument Matching Yielded:' #print '<-> Ordered Arguments:' + str(ordered_args) #print '<-> Named Arguments :' + str(named_args) - + resp = "" - + # For fault messages if ns: nsmethod = "%s:%s" % (ns, method) @@ -318,7 +319,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # there are none, because the split will return # [method] f = self.server.objmap[ns] - + # Look for the authorization method if self.server.config.authMethod != None: authmethod = self.server.config.authMethod @@ -359,7 +360,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): if "SOAPAction".lower() not in self.headers.keys() or \ self.headers["SOAPAction"] == "\"\"": self.headers["SOAPAction"] = method - + thread_id = thread.get_ident() _contexts[thread_id] = SOAPContext(header, body, attrs, data, @@ -374,11 +375,11 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): raise faultType("%s:Server" % NS.ENV_T, "Authorization failed.", "%s" % nsmethod) - + # If it's wrapped, some special action may be needed if isinstance(f, MethodSig): c = None - + if f.context: # retrieve context object c = _contexts[thread_id] @@ -389,9 +390,9 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): elif f.keywords: # This is lame, but have to de-unicode # keywords - + strkw = {} - + for (k, v) in kw.items(): strkw[str(k)] = v if c: @@ -408,7 +409,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): else: fr = apply(f, args, {}) - + if type(fr) == type(self) and \ isinstance(fr, voidType): resp = buildSOAP(kw = {'%sResponse' % method: fr}, @@ -423,7 +424,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # Clean up _contexts if _contexts.has_key(thread_id): del _contexts[thread_id] - + except Exception, e: import traceback info = sys.exc_info() @@ -558,7 +559,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.connection.shutdown(1) def do_GET(self): - + #print 'command ', self.command #print 'path ', self.path #print 'request_version', self.request_version @@ -567,7 +568,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): #print ' maintype', self.headers.maintype #print ' subtype ', self.headers.subtype #print ' params ', self.headers.plist - + path = self.path.lower() if path.endswith('wsdl'): method = 'wsdl' @@ -575,13 +576,13 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): if self.server.funcmap.has_key(namespace) \ and self.server.funcmap[namespace].has_key(method): function = self.server.funcmap[namespace][method] - else: + else: if namespace in self.server.objmap.keys(): function = self.server.objmap[namespace] l = method.split(".") for i in l: function = getattr(function, i) - + if function: self.send_response(200) self.send_header("Content-type", 'text/plain') @@ -589,7 +590,7 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): response = apply(function, ()) self.wfile.write(str(response)) return - + # return error self.send_response(200) self.send_header("Content-type", 'text/html') @@ -614,13 +615,17 @@ class SOAPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): ''') - + def log_message(self, format, *args): if self.server.log: BaseHTTPServer.BaseHTTPRequestHandler.\ log_message (self, format, *args) +class SOAPInsecureRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + '''Request handler that does load POSTed doctypes''' + ignore_ext = False + class SOAPServer(SOAPServerBase, SocketServer.TCPServer): @@ -679,19 +684,19 @@ class ThreadingSOAPServer(SOAPServerBase, SocketServer.ThreadingTCPServer): if hasattr(socket, "AF_UNIX"): class SOAPUnixSocketServer(SOAPServerBase, SocketServer.UnixStreamServer): - + def __init__(self, addr = 8000, RequestHandler = SOAPRequestHandler, log = 0, encoding = 'UTF-8', config = Config, namespace = None, ssl_context = None): - + # Test the encoding, raising an exception if it's not known if encoding != None: ''.encode(encoding) - + if ssl_context != None and not config.SSLserver: raise AttributeError, \ "SSL server not supported by this Python installation" - + self.namespace = namespace self.objmap = {} self.funcmap = {} @@ -699,8 +704,12 @@ if hasattr(socket, "AF_UNIX"): self.encoding = encoding self.config = config self.log = log - + self.allow_reuse_address= 1 - + SocketServer.UnixStreamServer.__init__(self, str(addr), RequestHandler) - + + + + + diff --git a/tests/testsclient.py b/tests/testsclient.py new file mode 100644 index 0000000..79aebc5 --- /dev/null +++ b/tests/testsclient.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +__docformat__ = 'restructuredtext en' +#!/usr/bin/env python +# coding:utf-8 + +from SOAPpy import SOAPProxy +server = SOAPProxy("http://localhost:8080/") +print server.echo("Hello world") + +# vim:set et sts=4 ts=4 tw=80: diff --git a/tests/testserver.py b/tests/testserver.py new file mode 100644 index 0000000..c436233 --- /dev/null +++ b/tests/testserver.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +__docformat__ = 'restructuredtext en' +#!/usr/bin/env python +# encoding:utf-8 +from SOAPpy import SOAPServer +def echo(s): + return s # repeats a string twice +server = SOAPServer(("0.0.0.0", 8080)) +server.registerFunction(echo) +server.serve_forever() + +# vim:set et sts=4 ts=4 tw=80: diff --git a/tests/vul_etcpasswd.txt b/tests/vul_etcpasswd.txt new file mode 100644 index 0000000..c4a915a --- /dev/null +++ b/tests/vul_etcpasswd.txt @@ -0,0 +1,21 @@ +POST / HTTP/1.0 +Host: localhost:8080 +User-agent: SOAPpy 0.12.0 (pywebsvcs.sf.net) +Content-type: text/xml; charset="UTF-8" +Content-length: 10000000 +SOAPAction: "echo" + + + ]> + + + +&xxe; aaa + + + diff --git a/vul_lol.txt b/vul_lol.txt new file mode 100644 index 0000000..b22d609 --- /dev/null +++ b/vul_lol.txt @@ -0,0 +1,32 @@ +POST / HTTP/1.0 +Host: localhost:8080 +User-agent: SOAPpy 0.12.0 (pywebsvcs.sf.net) +Content-type: text/xml; charset="UTF-8" +Content-length: 10000000 +SOAPAction: "echo" + + + + + + + + + + + +]> + + + +&lol9; + + + diff --git a/vul_ok.txt b/vul_ok.txt new file mode 100644 index 0000000..18a41f6 --- /dev/null +++ b/vul_ok.txt @@ -0,0 +1,21 @@ +POST / HTTP/1.0 +Host: localhost:8080 +User-agent: SOAPpy 0.12.0 (pywebsvcs.sf.net) +Content-type: text/xml; charset="UTF-8" +Content-length: 484 +SOAPAction: "echo" + + + + + +Hello world + + +