Skip to content

Commit

Permalink
Merge pull request #1726 from sebres/0.10-grave-fix-escape-tags-1st
Browse files Browse the repository at this point in the history
0.10 fix escape tags
  • Loading branch information
sebres committed Mar 21, 2017
2 parents 7a03c96 + 6ba0546 commit 1e67878
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 87 deletions.
19 changes: 17 additions & 2 deletions fail2ban/client/configparserinc.py
Expand Up @@ -32,7 +32,7 @@
if sys.version_info >= (3,2):

# SafeConfigParser deprecated from Python 3.2 (renamed to ConfigParser)
from configparser import ConfigParser as SafeConfigParser, \
from configparser import ConfigParser as SafeConfigParser, NoSectionError, \
BasicInterpolation

# And interpolation of __name__ was simply removed, thus we need to
Expand Down Expand Up @@ -60,7 +60,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map,
parser, option, accum, rest, section, map, depth)

else: # pragma: no cover
from ConfigParser import SafeConfigParser
from ConfigParser import SafeConfigParser, NoSectionError

# Gets the instance of the logger.
logSys = getLogger(__name__)
Expand Down Expand Up @@ -200,6 +200,21 @@ def get_defaults(self):
def get_sections(self):
return self._sections

def options(self, section, withDefault=True):
"""Return a list of option names for the given section name.
Parameter `withDefault` controls the include of names from section `[DEFAULT]`
"""
try:
opts = self._sections[section]
except KeyError:
raise NoSectionError(section)
if withDefault:
# mix it with defaults:
return set(opts.keys()) | set(self._defaults)
# only own option names:
return opts.keys()

def read(self, filenames, get_includes=True):
if not isinstance(filenames, list):
filenames = [ filenames ]
Expand Down
75 changes: 49 additions & 26 deletions fail2ban/client/configreader.py
Expand Up @@ -109,33 +109,44 @@ def _create_unshared(self, name=''):
self._cfg = ConfigReaderUnshared(**self._cfg_share_kwargs)

def sections(self):
if self._cfg is not None:
try:
return self._cfg.sections()
return []
except AttributeError:
return []

def has_section(self, sec):
if self._cfg is not None:
try:
return self._cfg.has_section(sec)
return False
except AttributeError:
return False

def merge_section(self, *args, **kwargs):
if self._cfg is not None:
return self._cfg.merge_section(*args, **kwargs)
def merge_section(self, section, *args, **kwargs):
try:
return self._cfg.merge_section(section, *args, **kwargs)
except AttributeError:
raise NoSectionError(section)

def options(self, section, withDefault=False):
"""Return a list of option names for the given section name.
def options(self, *args):
if self._cfg is not None:
return self._cfg.options(*args)
return {}
Parameter `withDefault` controls the include of names from section `[DEFAULT]`
"""
try:
return self._cfg.options(section, withDefault)
except AttributeError:
raise NoSectionError(section)

def get(self, sec, opt, raw=False, vars={}):
if self._cfg is not None:
try:
return self._cfg.get(sec, opt, raw=raw, vars=vars)
return None
except AttributeError:
raise NoSectionError(sec)

def getOptions(self, *args, **kwargs):
if self._cfg is not None:
return self._cfg.getOptions(*args, **kwargs)
return {}
def getOptions(self, section, *args, **kwargs):
try:
return self._cfg.getOptions(section, *args, **kwargs)
except AttributeError:
raise NoSectionError(section)


class ConfigReaderUnshared(SafeConfigParserWithIncludes):
Expand Down Expand Up @@ -297,23 +308,35 @@ def readexplicit(self):
self._create_unshared(self._file)
return SafeConfigParserWithIncludes.read(self._cfg, self._file)

def getOptions(self, pOpts):
def getOptions(self, pOpts, all=False):
# overwrite static definition options with init values, supplied as
# direct parameters from jail-config via action[xtra1="...", xtra2=...]:
if not pOpts:
pOpts = dict()
if self._initOpts:
if not pOpts:
pOpts = dict()
pOpts = _merge_dicts(pOpts, self._initOpts)
self._opts = ConfigReader.getOptions(
self, "Definition", self._configOpts, pOpts)
self._pOpts = pOpts
if self.has_section("Init"):
for opt in self.options("Init"):
v = self.get("Init", opt)
if not opt.startswith('known/') and opt != '__name__':
# get only own options (without options from default):
getopt = lambda opt: self.get("Init", opt)
for opt in self.options("Init", withDefault=False):
if opt == '__name__': continue
v = None
if not opt.startswith('known/'):
if v is None: v = getopt(opt)
self._initOpts['known/'+opt] = v
if not opt in self._initOpts:
if opt not in self._initOpts:
if v is None: v = getopt(opt)
self._initOpts[opt] = v
if all and self.has_section("Definition"):
# merge with all definition options (and options from default),
# bypass already converted option (so merge only new options):
for opt in self.options("Definition"):
if opt == '__name__' or opt in self._opts: continue
self._opts[opt] = self.get("Definition", opt)


def _convert_to_boolean(self, value):
return value.lower() in ("1", "yes", "true", "on")
Expand All @@ -336,12 +359,12 @@ def getCombOption(self, optname):

def getCombined(self, ignore=()):
combinedopts = self._opts
ignore = set(ignore).copy()
if self._initOpts:
combinedopts = _merge_dicts(self._opts, self._initOpts)
combinedopts = _merge_dicts(combinedopts, self._initOpts)
if not len(combinedopts):
return {}
# ignore conditional options:
ignore = set(ignore).copy()
for n in combinedopts:
cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n)
if cond:
Expand Down
6 changes: 3 additions & 3 deletions fail2ban/client/jailreader.py
Expand Up @@ -139,11 +139,11 @@ def getOptions(self):
filterName, self.__name, filterOpt,
share_config=self.share_config, basedir=self.getBaseDir())
ret = self.__filter.read()
# merge options from filter as 'known/...':
self.__filter.getOptions(self.__opts)
ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/')
if not ret:
raise JailDefError("Unable to read the filter %r" % filterName)
# merge options from filter as 'known/...' (all options unfiltered):
self.__filter.getOptions(self.__opts, all=True)
ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/')
else:
self.__filter = None
logSys.warning("No filter set for jail %s" % self.__name)
Expand Down
103 changes: 80 additions & 23 deletions fail2ban/server/action.py
Expand Up @@ -453,7 +453,7 @@ def escapeTag(value):
return value

@classmethod
def replaceTag(cls, query, aInfo, conditional='', cache=None, substRec=True):
def replaceTag(cls, query, aInfo, conditional='', cache=None):
"""Replaces tags in `query` with property values.
Parameters
Expand Down Expand Up @@ -481,9 +481,8 @@ def replaceTag(cls, query, aInfo, conditional='', cache=None, substRec=True):
# **Important**: don't replace if calling map - contains dynamic values only,
# no recursive tags, otherwise may be vulnerable on foreign user-input:
noRecRepl = isinstance(aInfo, CallingMap)
if noRecRepl:
subInfo = aInfo
else:
subInfo = aInfo
if not noRecRepl:
# substitute tags recursive (and cache if possible),
# first try get cached tags dictionary:
subInfo = csubkey = None
Expand Down Expand Up @@ -534,13 +533,86 @@ def substVal(m):
"unexpected too long replacement interpolation, "
"possible self referencing definitions in query: %s" % (query,))


# cache if possible:
if cache is not None:
cache[ckey] = value
#
return value

ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>\^\(\)\[\]{}$'"\n\r]""")
ESCAPE_VN_CRE = re.compile(r"\W")

@classmethod
def replaceDynamicTags(cls, realCmd, aInfo):
"""Replaces dynamical tags in `query` with property values.
**Important**
-------------
Because this tags are dynamic resp. foreign (user) input:
- values should be escaped (using "escape" as shell variable)
- no recursive substitution (no interpolation for <a<b>>)
- don't use cache
Parameters
----------
query : str
String with tags.
aInfo : dict
Tags(keys) and associated values for substitution in query.
Returns
-------
str
shell script as string or array with tags replaced (direct or as variables).
"""
# array for escaped vars:
varsDict = dict()

def escapeVal(tag, value):
# if the value should be escaped:
if cls.ESCAPE_CRE.search(value):
# That one needs to be escaped since its content is
# out of our control
tag = 'f2bV_%s' % cls.ESCAPE_VN_CRE.sub('_', tag)
varsDict[tag] = value # add variable
value = '$'+tag # replacement as variable
# replacement for tag:
return value

# substitution callable, used by interpolation of each tag
def substVal(m):
tag = m.group(1) # tagname from match
try:
value = aInfo[tag]
except KeyError:
# fallback (no or default replacement)
return ADD_REPL_TAGS.get(tag, m.group())
value = str(value) # assure string
# replacement for tag:
return escapeVal(tag, value)

# Replace normally properties of aInfo non-recursive:
realCmd = TAG_CRE.sub(substVal, realCmd)

# Replace ticket options (filter capture groups) non-recursive:
if '<' in realCmd:
tickData = aInfo.get("F-*")
if not tickData: tickData = {}
def substTag(m):
tag = mapTag2Opt(m.groups()[0])
try:
value = str(tickData[tag])
except KeyError:
return ""
return escapeVal("F_"+tag, value)

realCmd = FCUSTAG_CRE.sub(substTag, realCmd)

# build command corresponding "escaped" variables:
if varsDict:
realCmd = Utils.buildShellCmd(realCmd, varsDict)
return realCmd

def _processCmd(self, cmd, aInfo=None, conditional=''):
"""Executes a command with preliminary checks and substitutions.
Expand Down Expand Up @@ -605,21 +677,9 @@ def _processCmd(self, cmd, aInfo=None, conditional=''):
realCmd = self.replaceTag(cmd, self._properties,
conditional=conditional, cache=self.__substCache)

# Replace dynamical tags (don't use cache here)
# Replace dynamical tags, important - don't cache, no recursion and auto-escape here
if aInfo is not None:
realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional)
# Replace ticket options (filter capture groups) non-recursive:
if '<' in realCmd:
tickData = aInfo.get("F-*")
if not tickData: tickData = {}
def substTag(m):
tn = mapTag2Opt(m.groups()[0])
try:
return str(tickData[tn])
except KeyError:
return ""

realCmd = FCUSTAG_CRE.sub(substTag, realCmd)
realCmd = self.replaceDynamicTags(realCmd, aInfo)
else:
realCmd = cmd

Expand Down Expand Up @@ -653,8 +713,5 @@ def executeCmd(realCmd, timeout=60, **kwargs):
logSys.debug("Nothing to do")
return True

_cmd_lock.acquire()
try:
with _cmd_lock:
return Utils.executeCmd(realCmd, timeout, shell=True, output=False, **kwargs)
finally:
_cmd_lock.release()
1 change: 1 addition & 0 deletions fail2ban/server/actions.py
Expand Up @@ -290,6 +290,7 @@ class ActionInfo(CallingMap):

AI_DICT = {
"ip": lambda self: self.__ticket.getIP(),
"family": lambda self: self['ip'].familyStr,
"ip-rev": lambda self: self['ip'].getPTR(''),
"ip-host": lambda self: self['ip'].getHost(),
"fid": lambda self: self.__ticket.getID(),
Expand Down
5 changes: 5 additions & 0 deletions fail2ban/server/ipdns.py
Expand Up @@ -261,6 +261,11 @@ def addr(self):
def family(self):
return self._family

FAM2STR = {socket.AF_INET: 'inet4', socket.AF_INET6: 'inet6'}
@property
def familyStr(self):
return IPAddr.FAM2STR.get(self._family)

@property
def plen(self):
return self._plen
Expand Down

0 comments on commit 1e67878

Please sign in to comment.