Bandit [B411]: use defusedxml to prevent remote XML attacks

- putting xmlrpc stuff into koji.xmlrpcplus
- adding koji.xmlrpcplus.xmlrpc_server to refer
- replacing refs of original xmlrpc.client.dumps to enhanced
  koji.xmlrpcplus.dumps

fixes: #3964
This commit is contained in:
Yu Ming Zhu 2024-01-15 13:38:41 +00:00 committed by Tomas Kopecek
parent a96b5a9b07
commit f41b8c70a7
11 changed files with 46 additions and 38 deletions

View file

@ -23,7 +23,6 @@ import koji
from koji import parse_arches from koji import parse_arches
from koji import _ # noqa: F401 from koji import _ # noqa: F401
from koji.util import md5_constructor, to_list from koji.util import md5_constructor, to_list
from koji.xmlrpcplus import xmlrpc_client
# for compatibility with plugins based on older version of lib # for compatibility with plugins based on older version of lib
@ -208,7 +207,7 @@ class TaskWatcher(object):
error = None error = None
try: try:
self.session.getTaskResult(self.id) self.session.getTaskResult(self.id)
except (six.moves.xmlrpc_client.Fault, koji.GenericError) as e: except (koji.xmlrpcplus.Fault, koji.GenericError) as e:
error = e error = e
if error is None: if error is None:
# print("%s: complete" % self.str()) # print("%s: complete" % self.str())
@ -922,6 +921,6 @@ def truncate_string(s, length=47):
class DatetimeJSONEncoder(json.JSONEncoder): class DatetimeJSONEncoder(json.JSONEncoder):
def default(self, o): def default(self, o):
if isinstance(o, xmlrpc_client.DateTime): if isinstance(o, koji.xmlrpcplus.DateTime):
return str(o) return str(o)
return json.JSONEncoder.default(self, o) return json.JSONEncoder.default(self, o)

View file

@ -69,7 +69,7 @@ from requests.packages.urllib3.exceptions import MaxRetryError, HostChangedError
from six.moves import range, zip from six.moves import range, zip
from koji.tasks import parse_task_params from koji.tasks import parse_task_params
from koji.xmlrpcplus import Fault, dumps, getparser, loads, xmlrpc_client from koji.xmlrpcplus import DateTime, Fault, dumps, getparser, loads
from koji.util import deprecated from koji.util import deprecated
from . import util from . import util
from . import _version from . import _version
@ -3661,7 +3661,7 @@ def formatTime(value):
"""Format a timestamp so it looks nicer""" """Format a timestamp so it looks nicer"""
if not value and not isinstance(value, (int, float)): if not value and not isinstance(value, (int, float)):
return '' return ''
if isinstance(value, xmlrpc_client.DateTime): if isinstance(value, DateTime):
value = datetime.datetime.strptime(value.value, "%Y%m%dT%H:%M:%S") value = datetime.datetime.strptime(value.value, "%Y%m%dT%H:%M:%S")
elif isinstance(value, (int, float)): elif isinstance(value, (int, float)):
value = datetime.datetime.fromtimestamp(value) value = datetime.datetime.fromtimestamp(value)
@ -3684,7 +3684,7 @@ def formatTimeLong(value):
return '' return ''
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
t = dateutil.parser.parse(value) t = dateutil.parser.parse(value)
elif isinstance(value, xmlrpc_client.DateTime): elif isinstance(value, DateTime):
t = dateutil.parser.parse(value.value) t = dateutil.parser.parse(value.value)
elif isinstance(value, (int, float)): elif isinstance(value, (int, float)):
t = datetime.datetime.fromtimestamp(value) t = datetime.datetime.fromtimestamp(value)

View file

@ -6,9 +6,16 @@ from __future__ import absolute_import
import types import types
import defusedxml.xmlrpc as defusedxmlrpc
import re import re
import six import six
# importing here for references in koji by defused.xmlrpc.monkey_patch() below
import six.moves.xmlrpc_client as xmlrpc_client import six.moves.xmlrpc_client as xmlrpc_client
import six.moves.xmlrpc_server as xmlrpc_server # noqa: F401
# patching xmlrpc to protect against XML related attacks
defusedxmlrpc.monkey_patch()
# duplicate a few values that we need # duplicate a few values that we need
getparser = xmlrpc_client.getparser getparser = xmlrpc_client.getparser

View file

@ -47,7 +47,6 @@ import time
import types import types
import traceback import traceback
from urllib.parse import parse_qs from urllib.parse import parse_qs
import xmlrpc.client
import zipfile import zipfile
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
@ -513,7 +512,7 @@ class Task(object):
# handle older base64 encoded data # handle older base64 encoded data
xml_request = base64.b64decode(xml_request) xml_request = base64.b64decode(xml_request)
# note: loads accepts either bytes or string # note: loads accepts either bytes or string
params, method = xmlrpc.client.loads(xml_request) params, method = koji.xmlrpcplus.loads(xml_request)
return params return params
def getResult(self, raise_fault=True): def getResult(self, raise_fault=True):
@ -534,8 +533,8 @@ class Task(object):
try: try:
# If the result is a Fault, then loads will raise it # If the result is a Fault, then loads will raise it
# This is normally what we want to happen # This is normally what we want to happen
result, method = xmlrpc.client.loads(xml_result) result, method = koji.xmlrpcplus.loads(xml_result)
except xmlrpc.client.Fault as fault: except koji.xmlrpcplus.Fault as fault:
if raise_fault: if raise_fault:
raise raise
# Note that you can't really return a fault over xmlrpc, except by # Note that you can't really return a fault over xmlrpc, except by
@ -573,7 +572,7 @@ class Task(object):
# handle older base64 encoded data # handle older base64 encoded data
task['request'] = base64.b64decode(task['request']) task['request'] = base64.b64decode(task['request'])
# note: loads accepts either bytes or string # note: loads accepts either bytes or string
task['request'] = xmlrpc.client.loads(task['request'])[0] task['request'] = koji.xmlrpcplus.loads(task['request'])[0]
return results return results
def runCallbacks(self, cbtype, old_info, attr, new_val): def runCallbacks(self, cbtype, old_info, attr, new_val):
@ -13467,8 +13466,8 @@ class RootExports(object):
# handle older base64 encoded data # handle older base64 encoded data
val = base64.b64decode(val) val = base64.b64decode(val)
# note: loads accepts either bytes or string # note: loads accepts either bytes or string
data, method = xmlrpc.client.loads(val) data, method = koji.xmlrpcplus.loads(val)
except xmlrpc.client.Fault as fault: except koji.xmlrpcplus.Fault as fault:
data = fault data = fault
task[f] = data task[f] = data
yield task yield task
@ -13789,7 +13788,7 @@ class RootExports(object):
xmlrpc DateTime value""" xmlrpc DateTime value"""
context.session.assertPerm('admin') context.session.assertPerm('admin')
buildinfo = get_build(build, strict=True) buildinfo = get_build(build, strict=True)
if isinstance(ts, xmlrpc.client.DateTime): if isinstance(ts, koji.xmlrpcplus.DateTime):
# not recommended # not recommended
# the xmlrpclib.DateTime class is almost useless # the xmlrpclib.DateTime class is almost useless
try: try:

View file

@ -5,3 +5,4 @@ psycopg2-binary;python_version >= '3.0'
python-multilib python-multilib
python-qpid-proton<0.38.0;python_version < '3.0' python-qpid-proton<0.38.0;python_version < '3.0'
python-qpid-proton;python_version >= '3.0' python-qpid-proton;python_version >= '3.0'
defusedxml

View file

@ -15,7 +15,6 @@ import pprint
import smtplib import smtplib
import sys import sys
import time import time
import xmlrpc.client # for Fault
from email.mime import text as MIMEText from email.mime import text as MIMEText
from string import Template from string import Template
@ -1006,7 +1005,7 @@ def handle_prune():
session.untagBuildBypass(taginfo['id'], entry['build_id'], session.untagBuildBypass(taginfo['id'], entry['build_id'],
force=bypass or is_admin) force=bypass or is_admin)
untagged.setdefault(nvr, {})[tagname] = 1 untagged.setdefault(nvr, {})[tagname] = 1
except (xmlrpc.client.Fault, koji.GenericError) as e: except (koji.xmlrpcplus.Fault, koji.GenericError) as e:
print("Warning: untag operation failed: %s" % e) print("Warning: untag operation failed: %s" % e)
pass pass
# if action == 'keep' do nothing # if action == 'keep' do nothing
@ -1040,7 +1039,7 @@ def handle_prune():
print("Deleting untagged build: %s" % nvr) print("Deleting untagged build: %s" % nvr)
try: try:
session.deleteBuild(build_id, strict=True) session.deleteBuild(build_id, strict=True)
except (xmlrpc.client.Fault, koji.GenericError) as e: except (koji.xmlrpcplus.Fault, koji.GenericError) as e:
print("Warning: deletion failed: %s" % e) print("Warning: deletion failed: %s" % e)
# server issue # server issue
pass pass

View file

@ -5,7 +5,6 @@ import configparser
import datetime import datetime
import optparse import optparse
import os import os
import xmlrpc
import koji import koji
@ -124,7 +123,7 @@ def get_options():
def ensure_connection(session): def ensure_connection(session):
try: try:
ret = session.getAPIVersion() ret = session.getAPIVersion()
except xmlrpc.client.ProtocolError: except koji.xmlrpcplus.xmlrpc_client.ProtocolError:
error("Unable to connect to server") error("Unable to connect to server")
if ret != koji.API_VERSION: if ret != koji.API_VERSION:
warn("The server is at API version %d and the client is at %d" % (ret, koji.API_VERSION)) warn("The server is at API version %d and the client is at %d" % (ret, koji.API_VERSION))

View file

@ -1,12 +1,12 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import xmlrpc.client
from optparse import OptionParser from optparse import OptionParser
from koji.context import context from koji.context import context
import koji import koji
import kojihub.db import kojihub.db
import koji.xmlrpcplus
from kojihub.db import DeleteProcessor, QueryProcessor, BulkInsertProcessor from kojihub.db import DeleteProcessor, QueryProcessor, BulkInsertProcessor
@ -79,7 +79,7 @@ def clean_scratch_tasks(cursor, vacuum, test, age):
task_id = row['id'] task_id = row['id']
request = row['request'] request = row['request']
try: try:
params, _ = xmlrpc.client.loads(request) params, _ = koji.xmlrpcplus.xmlrpc_client.loads(request)
opts = params[2] opts = params[2]
if opts['scratch']: if opts['scratch']:
ids.append(task_id) ids.append(task_id)

View file

@ -38,7 +38,6 @@ import tempfile
import threading import threading
import time import time
import traceback import traceback
import xmlrpc.client
# urllib is required by the SCM class which is substituted into this file # urllib is required by the SCM class which is substituted into this file
# do not remove the import below # do not remove the import below
import urllib # noqa: F401 import urllib # noqa: F401
@ -46,6 +45,7 @@ import zipfile
from configparser import RawConfigParser from configparser import RawConfigParser
from optparse import OptionParser from optparse import OptionParser
from defusedxml import xmlrpc
import six # noqa: F401, needed for imported code import six # noqa: F401, needed for imported code
@ -53,6 +53,9 @@ MANAGER_PORT = 7000
KOJIKAMID = True KOJIKAMID = True
# patching xmlrpc to protect against XML related attacks
xmlrpc.monkey_patch()
# INSERT kojikamid dup # # INSERT kojikamid dup #
@ -635,7 +638,9 @@ def get_mgmt_server():
macaddr, gateway = find_net_info() macaddr, gateway = find_net_info()
logger.debug('found MAC address %s, connecting to %s:%s', logger.debug('found MAC address %s, connecting to %s:%s',
macaddr, gateway, MANAGER_PORT) macaddr, gateway, MANAGER_PORT)
server = xmlrpc.client.ServerProxy('http://%s:%s/' % (gateway, MANAGER_PORT), allow_none=True) server = xmlrpc.xmlrpc_client.ServerProxy(
'http://%s:%s/' % (gateway, MANAGER_PORT), allow_none=True
)
# we would set a timeout on the socket here, but that is apparently not # we would set a timeout on the socket here, but that is apparently not
# supported by python/cygwin/Windows # supported by python/cygwin/Windows
task_port = server.getPort(macaddr) task_port = server.getPort(macaddr)

View file

@ -35,8 +35,6 @@ import subprocess
import sys import sys
import threading import threading
import time import time
import xmlrpc
import xmlrpc.server
from contextlib import closing from contextlib import closing
from optparse import OptionParser from optparse import OptionParser
@ -45,6 +43,7 @@ import libxml2
import requests import requests
import koji import koji
from koji.xmlrpcplus import dumps, Fault, loads, xmlrpc_client, xmlrpc_server
import koji.util import koji.util
from koji.daemon import SCM, TaskManager from koji.daemon import SCM, TaskManager
# TaskHandlers are required to be imported, do not remove them # TaskHandlers are required to be imported, do not remove them
@ -295,15 +294,15 @@ def main(options, session):
# Tasks for handling VM lifecycle # Tasks for handling VM lifecycle
#################### ####################
class DaemonXMLRPCServer(xmlrpc.server.SimpleXMLRPCServer): class DaemonXMLRPCServer(xmlrpc_server.SimpleXMLRPCServer):
allow_reuse_address = True allow_reuse_address = True
def __init__(self, addr, port): def __init__(self, addr, port):
if sys.version_info[:2] <= (2, 4): if sys.version_info[:2] <= (2, 4):
xmlrpc.server.SimpleXMLRPCServer.__init__(self, (addr, port), xmlrpc_server.SimpleXMLRPCServer.__init__(self, (addr, port),
logRequests=False) logRequests=False)
else: else:
xmlrpc.server.SimpleXMLRPCServer.__init__(self, (addr, port), xmlrpc_server.SimpleXMLRPCServer.__init__(self, (addr, port),
logRequests=False, logRequests=False,
allow_none=True) allow_none=True)
self.logger = logging.getLogger('koji.vm.DaemonXMLRPCServer') self.logger = logging.getLogger('koji.vm.DaemonXMLRPCServer')
@ -312,7 +311,7 @@ class DaemonXMLRPCServer(xmlrpc.server.SimpleXMLRPCServer):
def server_close(self): def server_close(self):
self.active = False self.active = False
xmlrpc.server.SimpleXMLRPCServer.server_close(self) xmlrpc_server.SimpleXMLRPCServer.server_close(self)
def handle_while_active(self): def handle_while_active(self):
while self.active: while self.active:
@ -333,20 +332,20 @@ class DaemonXMLRPCServer(xmlrpc.server.SimpleXMLRPCServer):
# Copy and paste from SimpleXMLRPCServer, with the addition of passing # Copy and paste from SimpleXMLRPCServer, with the addition of passing
# allow_none=True to xmlrpclib.dumps() # allow_none=True to xmlrpclib.dumps()
def _marshaled_dispatch(self, data, dispatch_method=None): def _marshaled_dispatch(self, data, dispatch_method=None):
params, method = xmlrpc.client.loads(data) params, method = loads(data)
try: try:
if dispatch_method is not None: if dispatch_method is not None:
response = dispatch_method(method, params) response = dispatch_method(method, params)
else: else:
response = self._dispatch(method, params) response = self._dispatch(method, params)
response = (response,) response = (response,)
response = xmlrpc.client.dumps(response, methodresponse=1, allow_none=True) response = dumps(response, methodresponse=1, allow_none=True)
except xmlrpc.client.Fault as fault: except Fault as fault:
response = xmlrpc.client.dumps(fault) response = dumps(fault)
except Exception: except Exception:
# report exception back to server # report exception back to server
response = xmlrpc.client.dumps( response = dumps(
xmlrpc.client.Fault(1, "%s:%s" % (sys.exc_info()[0], sys.exc_info()[1])) Fault(1, "%s:%s" % (sys.exc_info()[0], sys.exc_info()[1]))
) )
return response return response
@ -480,7 +479,7 @@ class VMExecTask(BaseTaskHandler):
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
super(VMExecTask, self).__init__(*args, **kw) super(VMExecTask, self).__init__(*args, **kw)
self.task_manager = xmlrpc.client.ServerProxy( self.task_manager = xmlrpc_client.ServerProxy(
'http://%s:%s/' % (self.options.privaddr, self.options.portbase), allow_none=True) 'http://%s:%s/' % (self.options.privaddr, self.options.portbase), allow_none=True)
self.port = None self.port = None
self.server = None self.server = None

View file

@ -26,7 +26,6 @@ import re
import ssl import ssl
import stat import stat
import urllib import urllib
import xmlrpc.client
# a bunch of exception classes that explainError needs # a bunch of exception classes that explainError needs
from socket import error as socket_error from socket import error as socket_error
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
@ -35,6 +34,7 @@ import Cheetah.Template
import koji import koji
import koji.tasks import koji.tasks
from koji.xmlrpcplus import xmlrpc_client
themeInfo = {} themeInfo = {}
@ -726,7 +726,7 @@ a bug or a configuration issue."""
The web interface is having difficulty communicating with the main \ The web interface is having difficulty communicating with the main \
server. This most likely indicates a network issue.""" server. This most likely indicates a network issue."""
level = 1 level = 1
elif isinstance(error, (xmlrpc.client.ProtocolError, ExpatError)): elif isinstance(error, (xmlrpc_client.ProtocolError, ExpatError)):
str = """\ str = """\
The main server returned an invalid response. This could be caused by \ The main server returned an invalid response. This could be caused by \
a network issue or load issues on the server.""" a network issue or load issues on the server."""