mirror of
https://github.com/samba-team/samba.git
synced 2025-12-13 16:23:50 +03:00
An smb.conf can contain a 'wins hook' parameter, which names a script
to run when a WINS name is changed. The man page says
The second argument is the NetBIOS name. If the name is not a
legal name then the wins hook is not called. Legal names contain
only letters, digits, hyphens, underscores and periods.
but it turns out the legality check is not performed if the WINS
server in question is the source4 nbt one. It is not expected that
people will run this server, but they can. This is bad because the
name is passed unescaped into a shell command line, allowing command
injection.
For this test we don't care whether the WINS server is returning an
error code, just whether it is running the wins hook. The tests show
it often runs the hook it shouldn't, though some characters are
incidentally blocked because the name has to fit in a DN before it
gets to the hook, and DNs have a few syntactic restrictions (e.g.,
blocking '<', '>', and ';').
The source3 WINS server that is used by Samba when not run as a DC is
not affected and not here tested.
BUG: https://bugzilla.samba.org/show_bug.cgi?id=15903
Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
385 lines
12 KiB
Python
385 lines
12 KiB
Python
# Unix SMB/CIFS implementation.
|
||
# Copyright © Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
|
||
#
|
||
# This program is free software; you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation; either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
import os
|
||
import subprocess
|
||
from samba.tests import TestCase, check_help_consistency
|
||
import re
|
||
import stat
|
||
|
||
if 'SRCDIR_ABS' in os.environ:
|
||
BASEDIR = os.environ['SRCDIR_ABS']
|
||
else:
|
||
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||
'../../..'))
|
||
|
||
TEST_DIRS = [
|
||
"bootstrap",
|
||
"testdata",
|
||
"ctdb",
|
||
"dfs_server",
|
||
"pidl",
|
||
"auth",
|
||
"packaging",
|
||
"python",
|
||
"include",
|
||
"nsswitch",
|
||
"libcli",
|
||
"coverity",
|
||
"release-scripts",
|
||
"testprogs",
|
||
"bin",
|
||
"source3",
|
||
"docs-xml",
|
||
"buildtools",
|
||
"file_server",
|
||
"dynconfig",
|
||
"source4",
|
||
"tests",
|
||
"libds",
|
||
"selftest",
|
||
"lib",
|
||
"script",
|
||
"traffic",
|
||
"testsuite",
|
||
"libgpo",
|
||
"wintest",
|
||
"librpc",
|
||
]
|
||
|
||
|
||
EXCLUDE_USAGE = {
|
||
'script/autobuild.py', # defaults to mount /memdisk/
|
||
'script/bisect-test.py',
|
||
'ctdb/utils/etcd/ctdb_etcd_lock',
|
||
'selftest/filter-subunit',
|
||
'selftest/format-subunit',
|
||
'bin/gen_output.py', # too much output!
|
||
'source4/scripting/bin/gen_output.py',
|
||
'lib/ldb/tests/python/index.py',
|
||
'lib/ldb/tests/python/api.py',
|
||
'source4/selftest/tests.py',
|
||
'buildtools/bin/waf',
|
||
'testprogs/blackbox/wins_hook_test',
|
||
'selftest/tap2subunit',
|
||
'script/show_test_time',
|
||
'source4/scripting/bin/subunitrun',
|
||
'bin/samba_downgrade_db',
|
||
'source4/scripting/bin/samba_downgrade_db',
|
||
'source3/selftest/tests.py',
|
||
'selftest/tests.py',
|
||
'python/samba/subunit/run.py',
|
||
'bin/python/samba/subunit/run.py',
|
||
'lib/compression/tests/scripts/three-byte-hash',
|
||
}
|
||
|
||
EXCLUDE_HELP = {
|
||
'selftest/tap2subunit',
|
||
'wintest/test-s3.py',
|
||
'wintest/test-s4-howto.py',
|
||
'testprogs/blackbox/wins_hook_test',
|
||
}
|
||
|
||
|
||
EXCLUDE_DIRS = {
|
||
'source3/script/tests',
|
||
'python/examples',
|
||
'source4/dsdb/tests/python',
|
||
'bin/ab',
|
||
'bin/python/samba/tests',
|
||
'bin/python/samba/tests/blackbox',
|
||
'bin/python/samba/tests/dcerpc',
|
||
'bin/python/samba/tests/krb5',
|
||
'bin/python/samba/tests/ndr',
|
||
'python/samba/tests',
|
||
'python/samba/tests/bin',
|
||
'python/samba/tests/blackbox',
|
||
'python/samba/tests/dcerpc',
|
||
'python/samba/tests/krb5',
|
||
'python/samba/tests/ndr',
|
||
'bin/default/rust/vendor/unicode-width/scripts',
|
||
'bin/default/rust/vendor/rustls-webpki/tests',
|
||
}
|
||
|
||
|
||
def _init_git_file_finder():
|
||
"""Generate a function that quickly answers the question:
|
||
'is this a git file?'
|
||
"""
|
||
git_file_cache = set()
|
||
p = subprocess.run(['git',
|
||
'-C', BASEDIR,
|
||
'ls-files',
|
||
'-z'],
|
||
stdout=subprocess.PIPE)
|
||
if p.returncode == 0:
|
||
for fn in p.stdout.split(b'\0'):
|
||
git_file_cache.add(os.path.join(BASEDIR, fn.decode('utf-8')))
|
||
return git_file_cache.__contains__
|
||
|
||
|
||
is_git_file = _init_git_file_finder()
|
||
|
||
|
||
def script_iterator(d=BASEDIR, cache=None,
|
||
shebang_filter=None,
|
||
filename_filter=None,
|
||
subdirs=None):
|
||
if subdirs is None:
|
||
subdirs = TEST_DIRS
|
||
if not cache:
|
||
safename = re.compile(r'\W+').sub
|
||
for subdir in subdirs:
|
||
sd = os.path.join(d, subdir)
|
||
for root, dirs, files in os.walk(sd, followlinks=False):
|
||
for fn in files:
|
||
if fn.endswith('~'):
|
||
continue
|
||
if fn.endswith('.inst'):
|
||
continue
|
||
ffn = os.path.join(root, fn)
|
||
try:
|
||
s = os.stat(ffn)
|
||
except FileNotFoundError:
|
||
continue
|
||
if not s.st_mode & stat.S_IXUSR:
|
||
continue
|
||
if not (subdir == 'bin' or is_git_file(ffn)):
|
||
continue
|
||
|
||
if filename_filter is not None:
|
||
if not filename_filter(ffn):
|
||
continue
|
||
|
||
if shebang_filter is not None:
|
||
try:
|
||
f = open(ffn, 'rb')
|
||
except OSError as e:
|
||
print("could not open %s: %s" % (ffn, e))
|
||
continue
|
||
line = f.read(40)
|
||
f.close()
|
||
if not shebang_filter(line):
|
||
continue
|
||
|
||
name = safename('_', fn)
|
||
while name in cache:
|
||
name += '_'
|
||
cache[name] = ffn
|
||
|
||
return cache.items()
|
||
|
||
# For ELF we only look at /bin/* top level.
|
||
def elf_file_name(fn):
|
||
fn = fn.partition('bin/')[2]
|
||
return fn and '/' not in fn and 'test' not in fn and 'ldb' in fn
|
||
|
||
def elf_shebang(x):
|
||
return x[:4] == b'\x7fELF'
|
||
|
||
elf_cache = {}
|
||
def elf_iterator():
|
||
return script_iterator(BASEDIR, elf_cache,
|
||
shebang_filter=elf_shebang,
|
||
filename_filter=elf_file_name,
|
||
subdirs=['bin'])
|
||
|
||
|
||
perl_shebang = re.compile(br'#!.+perl').match
|
||
|
||
perl_script_cache = {}
|
||
def perl_script_iterator():
|
||
return script_iterator(BASEDIR, perl_script_cache, perl_shebang)
|
||
|
||
|
||
python_shebang = re.compile(br'#!.+python').match
|
||
|
||
python_script_cache = {}
|
||
def python_script_iterator():
|
||
return script_iterator(BASEDIR, python_script_cache, python_shebang)
|
||
|
||
|
||
class PerlScriptUsageTests(TestCase):
|
||
"""Perl scripts run without arguments should print a usage string,
|
||
not fail with a traceback.
|
||
"""
|
||
|
||
@classmethod
|
||
def initialise(cls):
|
||
for name, filename in perl_script_iterator():
|
||
print(name, filename)
|
||
|
||
|
||
class PythonScriptUsageTests(TestCase):
|
||
"""Python scripts run without arguments should print a usage string,
|
||
not fail with a traceback.
|
||
"""
|
||
|
||
@classmethod
|
||
def initialise(cls):
|
||
for name, filename in python_script_iterator():
|
||
# We add the actual tests after the class definition so we
|
||
# can give individual names to them, so we can have a
|
||
# knownfail list.
|
||
fn = filename.replace(BASEDIR, '').lstrip('/')
|
||
|
||
if fn in EXCLUDE_USAGE:
|
||
print("skipping %s (EXCLUDE_USAGE)" % filename)
|
||
continue
|
||
|
||
if os.path.dirname(fn) in EXCLUDE_DIRS:
|
||
print("skipping %s (EXCLUDE_DIRS)" % filename)
|
||
continue
|
||
|
||
def _f(self, filename=filename):
|
||
print(filename)
|
||
try:
|
||
p = subprocess.Popen(['python3', filename],
|
||
stderr=subprocess.PIPE,
|
||
stdout=subprocess.PIPE)
|
||
out, err = p.communicate(timeout=5)
|
||
except OSError as e:
|
||
self.fail("Error: %s" % e)
|
||
except subprocess.SubprocessError as e:
|
||
self.fail("Subprocess error: %s" % e)
|
||
|
||
err = err.decode('utf-8')
|
||
out = out.decode('utf-8')
|
||
self.assertNotIn('Traceback', err)
|
||
|
||
self.assertIn('usage', out.lower() + err.lower(),
|
||
'stdout:\n%s\nstderr:\n%s' % (out, err))
|
||
|
||
attr = 'test_%s' % name
|
||
if hasattr(cls, attr):
|
||
raise RuntimeError(f'Usage test ‘{attr}’ already exists!')
|
||
setattr(cls, attr, _f)
|
||
|
||
|
||
class HelpTestSuper(TestCase):
|
||
"""Python scripts run with -h or --help should print a help string,
|
||
and exit with success.
|
||
"""
|
||
check_return_code = True
|
||
check_consistency = True
|
||
check_contains_usage = True
|
||
check_multiline = True
|
||
check_merged_out_and_err = False
|
||
|
||
interpreter = None
|
||
|
||
options_start = None
|
||
options_end = None
|
||
def iterator(self):
|
||
raise NotImplementedError("Subclass this "
|
||
"and add an iterator function!")
|
||
|
||
@classmethod
|
||
def initialise(cls):
|
||
for name, filename in cls.iterator():
|
||
# We add the actual tests after the class definition so we
|
||
# can give individual names to them, so we can have a
|
||
# knownfail list.
|
||
fn = filename.replace(BASEDIR, '').lstrip('/')
|
||
|
||
if fn in EXCLUDE_HELP:
|
||
print("skipping %s (EXCLUDE_HELP)" % filename)
|
||
continue
|
||
|
||
if os.path.dirname(fn) in EXCLUDE_DIRS:
|
||
print("skipping %s (EXCLUDE_DIRS)" % filename)
|
||
continue
|
||
|
||
def _f(self, filename=filename):
|
||
print(filename)
|
||
for h in ('--help', '-h'):
|
||
cmd = [filename, h]
|
||
if self.interpreter:
|
||
cmd.insert(0, self.interpreter)
|
||
try:
|
||
p = subprocess.Popen(cmd,
|
||
stderr=subprocess.PIPE,
|
||
stdout=subprocess.PIPE)
|
||
out, err = p.communicate(timeout=5)
|
||
except OSError as e:
|
||
self.fail("Error: %s" % e)
|
||
except subprocess.SubprocessError as e:
|
||
self.fail("Subprocess error: %s" % e)
|
||
|
||
err = err.decode('utf-8')
|
||
out = out.decode('utf-8')
|
||
if self.check_merged_out_and_err:
|
||
out = "%s\n%s" % (out, err)
|
||
|
||
outl = out[:500].lower()
|
||
# NOTE:
|
||
# These assertions are heuristics, not policy.
|
||
# If your script fails this test when it shouldn't
|
||
# just add it to EXCLUDE_HELP above or change the
|
||
# heuristic.
|
||
|
||
# --help should produce:
|
||
# * multiple lines of help on stdout (not stderr),
|
||
# * including a "Usage:" string,
|
||
# * not contradict itself or repeat options,
|
||
# * and return success.
|
||
#print(out.encode('utf8'))
|
||
#print(err.encode('utf8'))
|
||
if self.check_consistency:
|
||
errors = check_help_consistency(out,
|
||
self.options_start,
|
||
self.options_end)
|
||
if errors is not None:
|
||
self.fail(errors)
|
||
|
||
if self.check_return_code:
|
||
self.assertEqual(p.returncode, 0,
|
||
"%s %s\nreturncode should not be %d\n"
|
||
"err:\n%s\nout:\n%s" %
|
||
(filename, h, p.returncode, err, out))
|
||
if self.check_contains_usage:
|
||
self.assertIn('usage', outl, 'lacks "Usage:"\n')
|
||
if self.check_multiline:
|
||
self.assertIn('\n', out, 'expected multi-line output')
|
||
|
||
attr = 'test_%s' % name
|
||
if hasattr(cls, attr):
|
||
raise RuntimeError(f'Usage test ‘{attr}’ already exists!')
|
||
setattr(cls, attr, _f)
|
||
|
||
|
||
class PythonScriptHelpTests(HelpTestSuper):
|
||
"""Python scripts run with -h or --help should print a help string,
|
||
and exit with success.
|
||
"""
|
||
iterator = python_script_iterator
|
||
interpreter = 'python3'
|
||
|
||
|
||
class ElfHelpTests(HelpTestSuper):
|
||
"""ELF binaries run with -h or --help should print a help string,
|
||
and exit with success.
|
||
"""
|
||
iterator = elf_iterator
|
||
check_return_code = False
|
||
check_merged_out_and_err = True
|
||
|
||
|
||
PerlScriptUsageTests.initialise()
|
||
PythonScriptUsageTests.initialise()
|
||
PythonScriptHelpTests.initialise()
|
||
ElfHelpTests.initialise()
|