Parse variables and fnmatch

This commit is contained in:
James Kasten
2015-07-13 23:18:33 -07:00
parent 89d810c06a
commit ec065e8b58
2 changed files with 50 additions and 64 deletions
@@ -64,7 +64,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
This class can adequately configure most typical configurations but This class can adequately configure most typical configurations but
is not ready to handle very complex configurations. is not ready to handle very complex configurations.
.. todo:: Always use self.parser.aug_get rather than self.aug.get
.. todo:: Verify permissions on configuration root... it is easier than .. todo:: Verify permissions on configuration root... it is easier than
checking permissions on each of the relative directories and less error checking permissions on each of the relative directories and less error
prone. prone.
@@ -332,7 +331,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
for name in name_match: for name in name_match:
args = self.aug.match(name + "/*") args = self.aug.match(name + "/*")
for arg in args: for arg in args:
host.add_name(self.aug.get(arg)) host.add_name(self.parser.get_arg(arg))
def _create_vhost(self, path): def _create_vhost(self, path):
"""Used by get_virtual_hosts to create vhost objects """Used by get_virtual_hosts to create vhost objects
@@ -346,7 +345,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
addrs = set() addrs = set()
args = self.aug.match(path + "/arg") args = self.aug.match(path + "/arg")
for arg in args: for arg in args:
addrs.add(common.Addr.fromstring(self.aug.get(arg))) addrs.add(common.Addr.fromstring(self.parser.get_arg(arg)))
is_ssl = False is_ssl = False
if self.parser.find_dir( if self.parser.find_dir(
@@ -514,7 +513,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
for addr in ssl_addr_p: for addr in ssl_addr_p:
old_addr = common.Addr.fromstring( old_addr = common.Addr.fromstring(
str(self.aug.get(addr))) str(self.parser.get_arg(addr)))
ssl_addr = old_addr.get_addr_obj("443") ssl_addr = old_addr.get_addr_obj("443")
self.aug.set(addr, str(ssl_addr)) self.aug.set(addr, str(ssl_addr))
ssl_addrs.add(ssl_addr) ssl_addrs.add(ssl_addr)
@@ -868,8 +867,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
errors.MisconfigurationError( errors.MisconfigurationError(
"Too many cert/key directives in vhost") "Too many cert/key directives in vhost")
cert = os.path.abspath(self.aug.get(cert_path[0])) cert = os.path.abspath(self.parser.get_arg(cert_path[0]))
key = os.path.abspath(self.aug.get(key_path[0])) key = os.path.abspath(self.parser.get_arg(key_path[0]))
c_k.add((cert, key, get_file_path(cert_path[0]))) c_k.add((cert, key, get_file_path(cert_path[0])))
return c_k return c_k
@@ -942,13 +941,19 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"and try again." % mod_name) "and try again." % mod_name)
self._enable_mod_debian(mod_name) self._enable_mod_debian(mod_name)
self.save_notes += "Enabled %s module in Apache" % mod_name
logger.debug("Enabled Apache %s module", mod_name)
# Modules can enable additional config files. Variables may be defined
# within these new configuration sections.
# Restart is not necessary as DUMP_RUN_CFG uses latest config.
self.parser.update_runtime_variables()
self.parser.modules.add(mod_name + "_module") self.parser.modules.add(mod_name + "_module")
self.parser.modules.add("mod_" + mod_name) self.parser.modules.add("mod_" + mod_name)
def _enable_mod_debian(self, mod_name): def _enable_mod_debian(self, mod_name):
"""Assumes mods-available, mods-enabled layout.""" """Assumes mods-available, mods-enabled layout."""
# TODO: This can be further updated to not require all files. # TODO: This can be further updated to not require all files.
if mod_name == "ssl": if mod_name == "ssl":
self._enable_mod_debian_files(["ssl.conf", "ssl.load"]) self._enable_mod_debian_files(["ssl.conf", "ssl.load"])
@@ -957,7 +962,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
else: else:
raise NotImplemented raise NotImplemented
def _enable_mod_debian_files(self, filenames): def _enable_mod_debian_files(self, filenames, mod_name):
"""Move over all required files into mods-enabled.""" """Move over all required files into mods-enabled."""
mods_available = os.path.join(self.parser.root, "mods-available") mods_available = os.path.join(self.parser.root, "mods-available")
mods_enabled = os.path.join(self.parser.root, "mods-enabled") mods_enabled = os.path.join(self.parser.root, "mods-enabled")
@@ -972,10 +977,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Register and symlink files # Register and symlink files
for filename in files: for filename in files:
enabled_path = os.path.join(mods_enabled, filename) enabled_path = os.path.join(mods_enabled, filename)
if os.path.isfile(enabled_path):
logger.debug(
"Error - enabling module %s, filepath already exists "
"%s", mod_name, enabled_path)
raise errors.PluginError("Error enabling module %s" % mod_name)
self.reverter.register_file_creation(False, enabled_path) self.reverter.register_file_creation(False, enabled_path)
os.symlink(os.path.join(mods_available, filename), enabled_path) os.symlink(os.path.join(mods_available, filename), enabled_path)
def mod_loaded(self, module): def mod_loaded(self, module):
"""Checks to see if mod_ssl is loaded """Checks to see if mod_ssl is loaded
+32 -55
View File
@@ -1,5 +1,6 @@
"""ApacheParser is a member object of the ApacheConfigurator class.""" """ApacheParser is a member object of the ApacheConfigurator class."""
import collections import collections
import fnmatch
import itertools import itertools
import logging import logging
import os import os
@@ -13,6 +14,7 @@ logger = logging.getLogger(__name__)
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}") arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
class ApacheParser(object): class ApacheParser(object):
@@ -29,7 +31,8 @@ class ApacheParser(object):
# https://httpd.apache.org/docs/2.4/mod/core.html#define # https://httpd.apache.org/docs/2.4/mod/core.html#define
# https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine # https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine
# This only handles invocation parameters and Define directives! # This only handles invocation parameters and Define directives!
self.variables = self._init_runtime_variables(ctl) self.variables = {}
self.update_runtime_variables(ctl)
# Find configuration root and make sure augeas can parse it. # Find configuration root and make sure augeas can parse it.
self.aug = aug self.aug = aug
@@ -67,11 +70,11 @@ class ApacheParser(object):
for match_name, match_filename in itertools.izip( for match_name, match_filename in itertools.izip(
iterator, iterator): iterator, iterator):
self.modules.add(self.aug.get(match_name)) self.modules.add(self.get_arg(match_name))
self.modules.add( self.modules.add(
os.path.basename(self.aug.get(match_filename))[:-2] + "c") os.path.basename(self.get_arg(match_filename))[:-2] + "c")
def _init_runtime_variables(self, ctl): def update_runtime_variables(self, ctl):
"""" """"
.. note:: Compile time variables (apache2ctl -V) are not used within the .. note:: Compile time variables (apache2ctl -V) are not used within the
@@ -94,7 +97,7 @@ class ApacheParser(object):
parts = match.partition("=") parts = match.partition("=")
variables[parts[0]] = parts[2] variables[parts[0]] = parts[2]
return variables self.variables = variables
def _get_runtime_cfg(self, ctl): def _get_runtime_cfg(self, ctl):
"""Get runtime configuration info. """Get runtime configuration info.
@@ -269,13 +272,15 @@ class ApacheParser(object):
ordered_matches = [] ordered_matches = []
# TODO: Wildcards should be included in alphabetical order
# https://httpd.apache.org/docs/2.4/mod/core.html#include
for match in matches: for match in matches:
dir = self.aug.get(match).lower() dir = self.aug.get(match).lower()
if dir == "include" or dir == "includeoptional": if dir == "include" or dir == "includeoptional":
# start[6:] to strip off /files # start[6:] to strip off /files
ordered_matches.extend(self.find_dir( ordered_matches.extend(self.find_dir(
directive, arg, self._get_include_path( directive, arg, self._get_include_path(
strip_dir(start[6:]), self.aug.get(match + "/arg")))) self.get_arg(match + "/arg"))))
else: else:
ordered_matches.extend(self.aug.match(match + arg_suffix)) ordered_matches.extend(self.aug.match(match + arg_suffix))
@@ -334,7 +339,7 @@ class ApacheParser(object):
return True return True
def _get_include_path(self, cur_dir, arg): def _get_include_path(self, arg):
"""Converts an Apache Include directive into Augeas path. """Converts an Apache Include directive into Augeas path.
Converts an Apache Include directive argument into an Augeas Converts an Apache Include directive argument into an Augeas
@@ -342,29 +347,12 @@ class ApacheParser(object):
.. todo:: convert to use os.path.join() .. todo:: convert to use os.path.join()
:param str cur_dir: current working directory
:param str arg: Argument of Include directive :param str arg: Argument of Include directive
:returns: Augeas path string :returns: Augeas path string
:rtype: str :rtype: str
""" """
# Sanity check argument - maybe
# Question: what can the attacker do with control over this string
# Effect parse file... maybe exploit unknown errors in Augeas
# If the attacker can Include anything though... and this function
# only operates on Apache real config data... then the attacker has
# already won.
# Perhaps it is better to simply check the permissions on all
# included files?
# check_config to validate apache config doesn't work because it
# would create a race condition between the check and this input
# TODO: Maybe... although I am convinced we have lost if
# Apache files can't be trusted. The augeas include path
# should be made to be exact.
# Check to make sure only expected characters are used <- maybe remove # Check to make sure only expected characters are used <- maybe remove
# validChars = re.compile("[a-zA-Z0-9.*?_-/]*") # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# matchObj = validChars.match(arg) # matchObj = validChars.match(arg)
@@ -374,11 +362,8 @@ class ApacheParser(object):
# Standardize the include argument based on server root # Standardize the include argument based on server root
if not arg.startswith("/"): if not arg.startswith("/"):
arg = cur_dir + arg # Normpath will condense ../
# conf/ is a special variable for ServerRoot in Apache arg = os.path.normpath(os.path.join(self.root, arg))
elif arg.startswith("conf/"):
arg = self.root + arg[4:]
# TODO: Test if Apache allows ../ or ~/ for Includes
# Attempts to add a transform to the file if one does not already exist # Attempts to add a transform to the file if one does not already exist
self._parse_file(arg) self._parse_file(arg)
@@ -386,46 +371,38 @@ class ApacheParser(object):
# Argument represents an fnmatch regular expression, convert it # Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex # Split up the path and convert each into an Augeas accepted regex
# then reassemble # then reassemble
if "*" in arg or "?" in arg: split_arg = arg.split("/")
split_arg = arg.split("/") for idx, split in enumerate(split_arg):
for idx, split in enumerate(split_arg): if any(char in fnmatch_chars for char in split):
# * and ? are the two special fnmatch characters # Turn it into a augeas regex
if "*" in split or "?" in split: # TODO: Can this instead be an augeas glob instead of regex
# Turn it into a augeas regex split_arg[idx] = ("* [label()=~regexp('%s')]" %
# TODO: Can this instead be an augeas glob instead of regex self.fnmatch_to_re(split))
split_arg[idx] = ("* [label()=~regexp('%s')]" % # Reassemble the argument
self.fnmatch_to_re(split)) arg = "/".join(split_arg)
# Reassemble the argument
arg = "/".join(split_arg)
# If the include is a directory, just return the directory as a file # If the include is a directory, just return the directory as a file
if arg.endswith("/"): if arg.endswith("/"):
return get_aug_path(arg[:len(arg)-1]) return get_aug_path(arg[:-1])
return get_aug_path(arg) return get_aug_path(arg)
def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use
"""Method converts Apache's basic fnmatch to regular expression. """Method converts Apache's basic fnmatch to regular expression.
Assumption - Configs are assumed to be well-formed and only writable by
privileged users.
https://apr.apache.org/docs/apr/2.0/apr__fnmatch_8h_source.html
http://apache2.sourcearchive.com/documentation/2.2.16-6/apr__fnmatch_8h_source.html
:param str clean_fn_match: Apache style filename match, similar to globs :param str clean_fn_match: Apache style filename match, similar to globs
:returns: regex suitable for augeas :returns: regex suitable for augeas
:rtype: str :rtype: str
""" """
# Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py # This strips off final /Z(?ms)
regex = "" return fnmatch.translate(clean_fn_match)[:-7]
for letter in clean_fn_match:
if letter == ".":
regex = regex + r"\."
elif letter == "*":
regex = regex + ".*"
# According to apache.org ? shouldn't appear
# but in case it is valid...
elif letter == "?":
regex = regex + "."
else:
regex = regex + letter
return regex
def _parse_file(self, filepath): def _parse_file(self, filepath):
"""Parse file with Augeas """Parse file with Augeas