File bnc_877009.patch of Package cobbler
From 45d63246ef628ab3f527168aa3e87e753e636cf0 Mon Sep 17 00:00:00 2001
From: Flavio Castelli <fcastelli@suse.com>
Date: Tue, 15 Jul 2014 11:36:30 +0200
Subject: [PATCH] Bug 877009 - VUL-0: CVE-2014-3225: cobbler: Local file
inclusion
---
cobbler/remote.py | 236 +++++++++++++++++++++++---------------
web/cobbler_web/views.py | 288 ++++++++++++++++++++++++-----------------------
2 files changed, 295 insertions(+), 229 deletions(-)
diff --git a/cobbler/remote.py b/cobbler/remote.py
index 5ce2350..bafde16 100644
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -68,6 +68,9 @@ REMAP_COMPAT = {
"netboot-enabled" : "netboot_enabled"
}
+KICKSTART_TEMPLATE_BASE_DIR = "/var/lib/cobbler/kickstarts/"
+KICKSTART_SNIPPET_BASE_DIR = "/var/lib/cobbler/snippets/"
+
class CobblerThread(Thread):
def __init__(self,event_id,remote,logatron,options):
Thread.__init__(self)
@@ -1889,107 +1892,164 @@ class CobblerXMLRPCInterface:
self.check_access(token,"sync")
return self.api.sync()
- def read_or_write_kickstart_template(self,kickstart_file,is_read,new_data,token):
+ def _validate_ks_template_path(self, path):
"""
- Allows the web app to be used as a kickstart file editor. For security
- reasons we will only allow kickstart files to be edited if they reside in
- /var/lib/cobbler/kickstarts/ or /etc/cobbler. This limits the damage
- doable by Evil who has a cobbler password but not a system password.
- Also if living in /etc/cobbler the file must be a kickstart file.
+ Validate a kickstart template file path
+
+ @param str path kickstart template file path
"""
- if is_read:
- what = "read_kickstart_template"
- else:
- what = "write_kickstart_template"
+ if path.find("..") != -1 or not path.startswith("/"):
+ utils.die(self.logger, "Invalid kickstart template file location %s" % path)
- self._log(what,name=kickstart_file,token=token)
- self.check_access(token,what,kickstart_file,is_read)
-
- if kickstart_file.find("..") != -1 or not kickstart_file.startswith("/"):
- utils.die(self.logger,"tainted file location")
+ if not path.startswith(KICKSTART_TEMPLATE_BASE_DIR):
+ error = "Invalid kickstart template file location %s, it is not inside %s" % (path, KICKSTART_TEMPLATE_BASE_DIR)
+ utils.die(self.logger, error)
- if not kickstart_file.startswith("/etc/cobbler/") and not kickstart_file.startswith("/var/lib/cobbler/kickstarts"):
- utils.die(self.logger, "unable to view or edit kickstart in this location")
-
- if kickstart_file.startswith("/etc/cobbler/"):
- if not kickstart_file.endswith(".ks") and not kickstart_file.endswith(".cfg"):
- # take care to not allow config files to be altered.
- utils.die(self.logger, "this does not seem to be a kickstart file")
- if not is_read and not os.path.exists(kickstart_file):
- utils.die(self.logger, "new files must go in /var/lib/cobbler/kickstarts")
-
- if is_read:
- fileh = open(kickstart_file,"r")
- data = fileh.read()
- fileh.close()
- return data
- else:
- if new_data == -1:
- # delete requested
- if not self.is_kickstart_in_use(kickstart_file,token):
- os.remove(kickstart_file)
- else:
- utils.die(self.logger, "attempt to delete in-use file")
- else:
- fileh = open(kickstart_file,"w+")
- fileh.write(new_data)
- fileh.close()
- return True
+ def read_kickstart_template_file(self, file_path, token):
+ """
+ Read a kickstart template file
- def read_or_write_snippet(self,snippet_file,is_read,new_data,token):
+ @param str file_path kickstart template file path
+ @param ? token
+ @return str file content
"""
- Allows the WebUI to be used as a snippet file editor. For security
- reasons we will only allow snippet files to be edited if they reside in
- /var/lib/cobbler/snippets.
+
+ what = "read_kickstart_template"
+ self._log(what, name=file_path, token=token)
+ self.check_access(token, what, file_path, True)
+ self._validate_ks_template_path(file_path)
+
+ fileh = open(file_path, "r")
+ data = fileh.read()
+ fileh.close()
+
+ return data
+
+ def write_kickstart_template_file(self, file_path, data, token):
"""
- # FIXME: duplicate code with kickstart view/edit
- # FIXME: need to move to API level functions
+ Write a kickstart template file
- if is_read:
- what = "read_snippet"
- else:
- what = "write_snippet"
+ @param str file_path kickstart template file path
+ @param str data new file content
+ @param ? token
+ @return bool if operation was successful
+ """
- self._log(what,name=snippet_file,token=token)
- self.check_access(token,what,snippet_file,is_read)
-
- if snippet_file.find("..") != -1 or not snippet_file.startswith("/"):
- utils.die(self.logger, "tainted file location")
+ what = "write_kickstart_template"
+ self._log(what, name=file_path, token=token)
+ self.check_access(token, what, file_path, True)
+ self._validate_ks_template_path(file_path)
- # FIXME: shouldn't we get snippetdir from the settings?
- if not snippet_file.startswith("/var/lib/cobbler/snippets"):
- utils.die(self.logger, "unable to view or edit snippet in this location")
-
- if is_read:
- fileh = open(snippet_file,"r")
- data = fileh.read()
- fileh.close()
- return data
+ try:
+ utils.mkdir(os.path.dirname(file_path))
+ except:
+ utils.die(self.logger, "unable to create directory for kickstart template at %s" % file_path)
+
+ fileh = open(file_path, "w+")
+ fileh.write(data)
+ fileh.close()
+
+ return True
+
+ def remove_kickstart_template_file(self, file_path, token):
+ """
+ Remove a kickstart template file
+
+ @param str file_path kickstart template file path
+ @param ? token
+ @return bool if operation was successful
+ """
+
+ what = "write_kickstart_template"
+ self._log(what, name=file_path, token=token)
+ self.check_access(token, what, file_path, True)
+ self._validate_ks_template_path(file_path)
+
+ if not self.is_kickstart_in_use(file_path, token):
+ os.remove(file_path)
else:
- if new_data == -1:
- # FIXME: no way to check if something is using it
- os.remove(snippet_file)
- else:
- # path_part(a,b) checks for the path b to be inside path a. It is
- # guaranteed to return either an empty string (meaning b is NOT inside
- # a), or a path starting with '/'. If the path ends with '/' the sub-path
- # is a directory so we don't write to it.
-
- # FIXME: shouldn't we get snippetdir from the settings?
- path_part = utils.path_tail("/var/lib/cobbler/snippets",snippet_file)
- if path_part != "" and path_part[-1] != "/":
- try:
- utils.mkdir(os.path.dirname(snippet_file))
- except:
- utils.die(self.logger, "unable to create directory for snippet file: '%s'" % snippet_file)
- fileh = open(snippet_file,"w+")
- fileh.write(new_data)
- fileh.close()
- else:
- utils.die(self.logger, "invalid snippet file specified: '%s'" % snippet_file)
- return True
+ utils.die(self.logger, "attempt to delete in-use file")
+
+ return True
+
+ # FIXME: duplicated code for kickstart and snippet
+ # FIXME: need to move to API level functions
+
+ def _validate_ks_snippet_path(self, path):
+
+ if path.find("..") != -1 or not path.startswith("/"):
+ utils.die(self.logger, "Invalid kickstart snippet file location %s" % path)
+
+ if not path.startswith(KICKSTART_SNIPPET_BASE_DIR):
+ error = "Invalid kickstart snippet file location %s, it is not inside %s" % (path, KICKSTART_SNIPPET_BASE_DIR)
+ utils.die(self.logger, error)
+
+ def read_kickstart_snippet_file(self, file_path, token):
+ """
+ Read a kickstart snippet file
+
+ @param str file_path kickstart snippet file path
+ @param ? token
+ @return str file content
+ """
+
+ what = "read_kickstart_snippet"
+ self._log(what, name=file_path, token=token)
+ self.check_access(token, what, file_path, True)
+ self._validate_ks_snippet_path(file_path)
+
+ fileh = open(file_path, "r")
+ data = fileh.read()
+ fileh.close()
+
+ return data
+
+ def write_kickstart_snippet_file(self, file_path, data, token):
+ """
+ Write a kickstart snippet file
+
+ @param str file_path kickstart snippet file path
+ @param str data new file content
+ @param ? token
+ @return bool if operation was successful
+ """
+
+ what = "write_kickstart_snippet"
+ self._log(what, name=file_path, token=token)
+ self.check_access(token, what, file_path, True)
+ self._validate_ks_snippet_path(file_path)
+
+ try:
+ utils.mkdir(os.path.dirname(file_path))
+ except:
+ utils.die(self.logger, "unable to create directory for snippet at %s" % file_path)
+
+ fileh = open(file_path, "w+")
+ fileh.write(data)
+ fileh.close()
+
+ return True
+
+ def remove_kickstart_snippet_file(self, file_path, token):
+ """
+ Remove a kickstart snippet file
+
+ @param str file_path kickstart snippet file path
+ @param ? token
+ @return bool if operation was successful
+ """
+
+ what = "write_kickstart_snippet"
+ self._log(what, name=file_path, token=token)
+ self.check_access(token, what, file_path, True)
+ self._validate_ks_snippet_path(file_path)
+ # FIXME: could check if snippet is in use
+ snippet_file_path = KICKSTART_SNIPPET_BASE_DIR + file_path
+ os.remove(file_path)
+
+ return True
def power_system(self,object_id,power=None,token=None,logger=None):
"""
diff --git a/web/cobbler_web/views.py b/web/cobbler_web/views.py
index 692ba65..72ceb14 100644
--- a/web/cobbler_web/views.py
+++ b/web/cobbler_web/views.py
@@ -632,178 +632,184 @@ def import_run(request):
# ======================================================================
def ksfile_list(request, page=None):
- """
- List all kickstart templates and link to their edit pages.
- """
- if not test_user_authenticated(request): return login(request, next="/cobbler_web/ksfile/list", expired=True)
- ksfiles = remote.get_kickstart_templates(request.session['token'])
-
- ksfile_list = []
- for ksfile in ksfiles:
- if ksfile.startswith("/var/lib/cobbler/kickstarts") or ksfile.startswith("/etc/cobbler"):
- ksfile_list.append((ksfile,ksfile.replace('/var/lib/cobbler/kickstarts/',''),'editable'))
- elif ksfile.startswith("http://") or ksfile.startswith("ftp://"):
- ksfile_list.append((ksfile,ksfile,'','viewable'))
- else:
- ksfile_list.append((ksfile,ksfile,None))
+ """
+ List all kickstart templates and link to their edit pages.
+ """
+ if not test_user_authenticated(request):
+ return login(request, next="/cobbler_web/ksfile/list", expired=True)
+ ksfiles = remote.get_kickstart_templates(request.session['token'])
+
+ ksfile_list = []
+ base_dir = "/var/lib/cobbler/kickstarts/"
+ for ksfile in ksfiles:
+ if ksfile.startswith(base_dir):
+ ksfile_list.append((ksfile, ksfile.replace(base_dir, ''), 'editable'))
+ else:
+ return error_page(request, "Invalid kickstart template at %s, outside %s" % (ksfile, base_dir))
- t = get_template('ksfile_list.tmpl')
- html = t.render(RequestContext(request,{
- 'what':'ksfile',
- 'ksfiles': ksfile_list,
- 'version': remote.extended_version(request.session['token'])['version'],
- 'username': username,
- 'item_count': len(ksfile_list[0]),
- }))
- return HttpResponse(html)
+ t = get_template('ksfile_list.tmpl')
+ html = t.render(RequestContext(request, {
+ 'what': 'ksfile',
+ 'ksfiles': ksfile_list,
+ 'version': remote.extended_version(request.session['token'])['version'],
+ 'username': username,
+ 'item_count': len(ksfile_list[0]),
+ }))
+ return HttpResponse(html)
# ======================================================================
@csrf_protect
def ksfile_edit(request, ksfile_name=None, editmode='edit'):
- """
- This is the page where a kickstart file is edited.
- """
- if not test_user_authenticated(request): return login(request, next="/cobbler_web/ksfile/edit/file:%s" % ksfile_name, expired=True)
- if editmode == 'edit':
- editable = False
- else:
- editable = True
- deleteable = False
- ksdata = ""
- if not ksfile_name is None:
- editable = remote.check_access_no_fail(request.session['token'], "modify_kickstart", ksfile_name)
- deleteable = not remote.is_kickstart_in_use(ksfile_name, request.session['token'])
- ksdata = remote.read_or_write_kickstart_template(ksfile_name, True, "", request.session['token'])
-
- t = get_template('ksfile_edit.tmpl')
- html = t.render(RequestContext(request,{
- 'ksfile_name' : ksfile_name,
- 'deleteable' : deleteable,
- 'ksdata' : ksdata,
- 'editable' : editable,
- 'editmode' : editmode,
- 'version' : remote.extended_version(request.session['token'])['version'],
- 'username' : username
- }))
- return HttpResponse(html)
+ """
+ This is the page where a kickstart file is edited.
+ """
+ if not test_user_authenticated(request):
+ return login(request, next="/cobbler_web/ksfile/edit/file:%s" % ksfile_name, expired=True)
+ if editmode == 'edit':
+ editable = False
+ else:
+ editable = True
+ deleteable = False
+ ksdata = ""
+ if not ksfile_name is None:
+ editable = remote.check_access_no_fail(request.session['token'], "modify_kickstart", ksfile_name)
+ deleteable = not remote.is_kickstart_in_use(ksfile_name, request.session['token'])
+ ksdata = remote.read_kickstart_template(ksfile_name, request.session['token'])
+
+ t = get_template('ksfile_edit.tmpl')
+ html = t.render(RequestContext(request, {
+ 'ksfile_name': ksfile_name,
+ 'deleteable': deleteable,
+ 'ksdata': ksdata,
+ 'editable': editable,
+ 'editmode': editmode,
+ 'version': remote.extended_version(request.session['token'])['version'],
+ 'username': username
+ }))
+ return HttpResponse(html)
# ======================================================================
@require_POST
@csrf_protect
def ksfile_save(request):
- """
- This page processes and saves edits to a kickstart file.
- """
- if not test_user_authenticated(request): return login(request, next="/cobbler_web/ksfile/list", expired=True)
- # FIXME: error checking
+ """
+ This page processes and saves edits to a kickstart file.
+ """
+ if not test_user_authenticated(request):
+ return login(request, next="/cobbler_web/ksfile/list", expired=True)
+ # FIXME: error checking
- editmode = request.POST.get('editmode', 'edit')
- ksfile_name = request.POST.get('ksfile_name', None)
- ksdata = request.POST.get('ksdata', "").replace('\r\n','\n')
+ editmode = request.POST.get('editmode', 'edit')
+ ksfile_name = request.POST.get('ksfile_name', None)
+ ksdata = request.POST.get('ksdata', "").replace('\r\n', '\n')
- if ksfile_name == None:
- return HttpResponse("NO KSFILE NAME SPECIFIED")
- if editmode != 'edit':
- ksfile_name = "/var/lib/cobbler/kickstarts/" + ksfile_name
+ if ksfile_name == None:
+ return HttpResponse("NO KSFILE NAME SPECIFIED")
+ if editmode != 'edit':
+ ksfile_name = "/var/lib/cobbler/kickstarts/" + ksfile_name
- delete1 = request.POST.get('delete1', None)
- delete2 = request.POST.get('delete2', None)
+ delete1 = request.POST.get('delete1', None)
+ delete2 = request.POST.get('delete2', None)
- if delete1 and delete2:
- remote.read_or_write_kickstart_template(ksfile_name, False, -1, request.session['token'])
- return HttpResponseRedirect('/cobbler_web/ksfile/list')
- else:
- remote.read_or_write_kickstart_template(ksfile_name,False,ksdata,request.session['token'])
- return HttpResponseRedirect('/cobbler_web/ksfile/edit/file:%s' % ksfile_name)
+ if delete1 and delete2:
+ remote.remove_kickstart_template(ksfile_name, request.session['token'])
+ return HttpResponseRedirect('/cobbler_web/ksfile/list')
+ else:
+ remote.write_kickstart_template(ksfile_name, ksdata, request.session['token'])
+ return HttpResponseRedirect('/cobbler_web/ksfile/list')
# ======================================================================
def snippet_list(request, page=None):
- """
- This page lists all available snippets and has links to edit them.
- """
- if not test_user_authenticated(request): return login(request, next="/cobbler_web/snippet/list", expired=True)
- snippets = remote.get_snippets(request.session['token'])
-
- snippet_list = []
- for snippet in snippets:
- if snippet.startswith("/var/lib/cobbler/snippets"):
- snippet_list.append((snippet,snippet.replace("/var/lib/cobbler/snippets/",""),'editable'))
- else:
- snippet_list.append((snippet,snippet,None))
-
- t = get_template('snippet_list.tmpl')
- html = t.render(RequestContext(request,{
- 'what' : 'snippet',
- 'snippets' : snippet_list,
- 'version' : remote.extended_version(request.session['token'])['version'],
- 'username' : username
- }))
- return HttpResponse(html)
+ """
+ This page lists all available snippets and has links to edit them.
+ """
+ if not test_user_authenticated(request):
+ return login(request, next="/cobbler_web/snippet/list", expired=True)
+ snippets = remote.get_snippets(request.session['token'])
+
+ snippet_list = []
+ base_dir = "/var/lib/cobbler/snippets/"
+ for snippet in snippets:
+ if snippet.startswith(base_dir):
+ snippet_list.append((snippet, snippet.replace(base_dir, ""), 'editable'))
+ else:
+ return error_page(request, "Invalid snippet at %s, outside %s" % (snippet, base_dir))
+
+ t = get_template('snippet_list.tmpl')
+ html = t.render(RequestContext(request, {
+ 'what': 'snippet',
+ 'snippets': snippet_list,
+ 'version': remote.extended_version(request.session['token'])['version'],
+ 'username': username
+ }))
+ return HttpResponse(html)
# ======================================================================
@csrf_protect
def snippet_edit(request, snippet_name=None, editmode='edit'):
- """
- This page edits a specific snippet.
- """
- if not test_user_authenticated(request): return login(request, next="/cobbler_web/edit/file:%s" % snippet_name, expired=True)
- if editmode == 'edit':
- editable = False
- else:
- editable = True
- deleteable = False
- snippetdata = ""
- if not snippet_name is None:
- editable = remote.check_access_no_fail(request.session['token'], "modify_snippet", snippet_name)
- deleteable = True
- snippetdata = remote.read_or_write_snippet(snippet_name, True, "", request.session['token'])
-
- t = get_template('snippet_edit.tmpl')
- html = t.render(RequestContext(request,{
- 'snippet_name' : snippet_name,
- 'deleteable' : deleteable,
- 'snippetdata' : snippetdata,
- 'editable' : editable,
- 'editmode' : editmode,
- 'version' : remote.extended_version(request.session['token'])['version'],
- 'username' : username
- }))
- return HttpResponse(html)
+ """
+ This page edits a specific snippet.
+ """
+ if not test_user_authenticated(request):
+ return login(request, next="/cobbler_web/edit/file:%s" % snippet_name, expired=True)
+ if editmode == 'edit':
+ editable = False
+ else:
+ editable = True
+ deleteable = False
+ snippetdata = ""
+ if not snippet_name is None:
+ editable = remote.check_access_no_fail(request.session['token'], "modify_snippet", snippet_name)
+ deleteable = True
+ snippetdata = remote.read_kickstart_snippet(snippet_name, request.session['token'])
+
+ t = get_template('snippet_edit.tmpl')
+ html = t.render(RequestContext(request, {
+ 'snippet_name': snippet_name,
+ 'deleteable': deleteable,
+ 'snippetdata': snippetdata,
+ 'editable': editable,
+ 'editmode': editmode,
+ 'version': remote.extended_version(request.session['token'])['version'],
+ 'username': username
+ }))
+ return HttpResponse(html)
# ======================================================================
@require_POST
@csrf_protect
def snippet_save(request):
- """
- This snippet saves a snippet once edited.
- """
- if not test_user_authenticated(request): return login(request, next="/cobbler_web/snippet/list", expired=True)
- # FIXME: error checking
-
- editmode = request.POST.get('editmode', 'edit')
- snippet_name = request.POST.get('snippet_name', None)
- snippetdata = request.POST.get('snippetdata', "").replace('\r\n','\n')
-
- if snippet_name == None:
- return HttpResponse("NO SNIPPET NAME SPECIFIED")
- if editmode != 'edit':
- if snippet_name.find("/var/lib/cobbler/snippets/") != 0:
- snippet_name = "/var/lib/cobbler/snippets/" + snippet_name
-
- delete1 = request.POST.get('delete1', None)
- delete2 = request.POST.get('delete2', None)
-
- if delete1 and delete2:
- remote.read_or_write_snippet(snippet_name, False, -1, request.session['token'])
- return HttpResponseRedirect('/cobbler_web/snippet/list')
- else:
- remote.read_or_write_snippet(snippet_name,False,snippetdata,request.session['token'])
- return HttpResponseRedirect('/cobbler_web/snippet/edit/file:%s' % snippet_name)
+ """
+ This snippet saves a snippet once edited.
+ """
+ if not test_user_authenticated(request):
+ return login(request, next="/cobbler_web/snippet/list", expired=True)
+ # FIXME: error checking
+
+ editmode = request.POST.get('editmode', 'edit')
+ snippet_name = request.POST.get('snippet_name', None)
+ snippetdata = request.POST.get('snippetdata', "").replace('\r\n', '\n')
+
+ if snippet_name == None:
+ return HttpResponse("NO SNIPPET NAME SPECIFIED")
+ if editmode != 'edit':
+ if snippet_name.find("/var/lib/cobbler/snippets/") != 0:
+ snippet_name = "/var/lib/cobbler/snippets/" + snippet_name
+
+ delete1 = request.POST.get('delete1', None)
+ delete2 = request.POST.get('delete2', None)
+
+ if delete1 and delete2:
+ remote.remove_kickstart_snippet(snippet_name, request.session['token'])
+ return HttpResponseRedirect('/cobbler_web/snippet/list')
+ else:
+ remote.write_kickstart_snippet(snippet_name, snippetdata, request.session['token'])
+ return HttpResponseRedirect('/cobbler_web/snippet/list')
# ======================================================================
--
1.8.4.5