Skip to content

Commit 717633e

Browse files
authored
Refactoring for error messages and security fix for path echoing (#636)
* Refactoring for error messages and security fix for path echoing * Test match string fix
1 parent 9389c94 commit 717633e

File tree

3 files changed

+33
-25
lines changed

3 files changed

+33
-25
lines changed

lib/error_helpers.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ def end_error(*errors):
1111

1212

1313
def format_error(*errors):
14-
err = 'Error: '
14+
err = ''
1515

1616
for error in errors:
17-
err += str(error)
17+
err += str(error) + "\n"
1818

1919
error_string = f"""
2020
\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n
21-
{err}
21+
Error: {err}
2222
\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n
2323
{traceback.format_exc()}
2424
\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n
@@ -30,11 +30,15 @@ def format_error(*errors):
3030
def log_error(*errors):
3131
error_log_file = GlobalConfig().config['machine']['error_log_file']
3232

33+
err = ''
34+
for error in errors:
35+
err += str(error) + "\n"
36+
3337
if error_log_file:
3438
try:
3539
with open(error_log_file, 'a', encoding='utf-8') as file:
3640
print('\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n', file=file)
37-
print('Error: ', *errors, file=file)
41+
print('Error: ', err, file=file)
3842
print('\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n', file=file)
3943
print(traceback.format_exc(), file=file)
4044
print('\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n', file=file)
@@ -46,6 +50,6 @@ def log_error(*errors):
4650
'\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n', file=sys.stderr)
4751
print(traceback.format_exc(), file=sys.stderr)
4852
print('\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n', file=sys.stderr)
49-
print('Error: ', *errors, file=sys.stderr)
53+
print('Error: ', err, file=sys.stderr)
5054
print('\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0_o >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n',
5155
TerminalColors.ENDC, file=sys.stderr)

runner.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def arrows(text):
4848
# path with `..`, symbolic links or similar.
4949
# We always return the same error message including the path and file parameter, never `filename` as
5050
# otherwise we might disclose if certain files exist or not.
51-
def join_paths(path, path2, mode=None):
51+
def join_paths(path, path2, mode='file'):
5252
filename = os.path.realpath(os.path.join(path, path2))
5353

5454
# If the original path is a symlink we need to resolve it.
@@ -59,23 +59,23 @@ def join_paths(path, path2, mode=None):
5959
return filename
6060

6161
if not filename.startswith(path):
62-
raise ValueError(f"{path2} not in {path}")
62+
raise ValueError(f"{path2} must not be in folder above {path}")
6363

6464
# To double check we also check if it is in the files allow list
6565

6666
if mode == 'file':
6767
folder_content = [str(item) for item in Path(path).rglob("*") if item.is_file()]
68-
elif mode == 'dir':
68+
elif mode == 'directory':
6969
folder_content = [str(item) for item in Path(path).rglob("*") if item.is_dir()]
7070
else:
71-
folder_content = [str(item) for item in Path(path).rglob("*")]
71+
raise RuntimeError(f"Unknown mode supplied for join_paths: {mode}")
7272

7373
if filename not in folder_content:
74-
raise ValueError(f"{path2} not in {path}")
74+
raise ValueError(f"{mode.capitalize()} '{path2}' not in '{path}'")
7575

7676
# Another way to implement this. This is checking the third time but we want to be extra secure 👾
7777
if Path(path).resolve(strict=True) not in Path(path, path2).resolve(strict=True).parents:
78-
raise ValueError(f"{path2} not in {path}")
78+
raise ValueError(f"{mode.capitalize()} '{path2}' not in folder '{path}'")
7979

8080
if os.path.exists(filename):
8181
return filename
@@ -574,7 +574,7 @@ def build_docker_images(self):
574574
self.__notes_helper.add_note({'note': f"Building {service['image']}", 'detail_name': '[NOTES]', 'timestamp': int(time.time_ns() / 1_000)})
575575

576576
# Make sure the context docker file exists and is not trying to escape some root. We don't need the returns
577-
context_path = join_paths(self._folder, context, 'dir')
577+
context_path = join_paths(self._folder, context, 'directory')
578578
join_paths(context_path, dockerfile, 'file')
579579

580580
docker_build_command = ['docker', 'run', '--rm',
@@ -742,16 +742,19 @@ def setup_services(self):
742742
# We always assume the format to be ./dir:dir:[flag] as if we allow none bind mounts people
743743
# could create volumes that would linger on our system.
744744
path = os.path.realpath(os.path.join(self._folder, vol[0]))
745-
if not os.path.exists(path):
746-
raise RuntimeError(f"Service '{service_name}' volume path does not exist: {path}")
747745

748746
# Check that the path starts with self._folder
747+
# We need to do this first, as telling first if path exists or not will tell about our filesystem structure
749748
if not path.startswith(self._folder):
750-
raise RuntimeError(f"Service '{service_name}' trying to escape folder: {path}")
749+
raise RuntimeError(f"Service '{service_name}' volume path ({vol[0]}) is outside allowed folder: {path}")
750+
751+
if not os.path.exists(path):
752+
raise RuntimeError(f"Service '{service_name}' volume path does not exist: {path}")
751753

752754
# To double check we also check if it is in the files allow list
753-
if path not in [str(item) for item in Path(self._folder).rglob("*")]:
754-
raise RuntimeError(f"Service '{service_name}' volume '{path}' not in allowed file list")
755+
allowed_files_list = [str(item) for item in Path(self._folder).rglob("*")]
756+
if path not in allowed_files_list:
757+
raise RuntimeError(f"Service '{service_name}' volume '{path}' not in allowed file list:\n{chr(10).join(allowed_files_list)}\n\nOnly files from the supplied repository are allowed.")
755758

756759
if len(vol) == 3:
757760
if vol[2] != 'ro':
@@ -1505,13 +1508,13 @@ def run(self):
15051508
print('####################################################################################\n\n', TerminalColors.ENDC)
15061509

15071510
except FileNotFoundError as e:
1508-
error_helpers.log_error('File or executable not found', e, runner._run_id)
1511+
error_helpers.log_error('File or executable not found', e, "\n", f"Run-ID: {runner._run_id}")
15091512
except subprocess.CalledProcessError as e:
1510-
error_helpers.log_error('Command failed', 'Stdout:', e.stdout, 'Stderr:', e.stderr, runner._run_id)
1513+
error_helpers.log_error('Command failed', 'Stdout:', e.stdout, 'Stderr:', e.stderr, "\n", f"Run-ID: {runner._run_id}")
15111514
except RuntimeError as e:
1512-
error_helpers.log_error('RuntimeError occured in runner.py: ', e, runner._run_id)
1515+
error_helpers.log_error('RuntimeError occured in runner.py: ', e, "\n", f"Run-ID: {runner._run_id}")
15131516
except BaseException as e:
1514-
error_helpers.log_error('Base exception occured in runner.py: ', e, runner._run_id)
1517+
error_helpers.log_error('Base exception occured in runner.py: ', e, "\n", f"Run-ID: {runner._run_id}")
15151518
finally:
15161519
if args.print_logs:
15171520
for container_id_outer, std_out in runner.get_logs().items():

tests/test_volume_loading.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ def test_volume_load_no_escape():
5050
container_running = check_if_container_running('test-container')
5151
runner.cleanup()
5252

53-
expected_error = 'trying to escape folder: /etc/passwd'
54-
assert expected_error in str(e.value), Tests.assertion_info(expected_error, str(e.value))
53+
expected_error = 'Service \'test-container\' volume path (/etc/passwd) is outside allowed folder:'
54+
assert str(e.value).startswith(expected_error), Tests.assertion_info(expected_error, str(e.value))
5555
assert container_running is False, Tests.assertion_info('test-container stopped', 'test-container was still running!')
5656

5757
def create_tmp_dir():
@@ -106,6 +106,7 @@ def test_load_files_from_within_gmt():
106106
def test_symlinks_should_fail():
107107
tmp_dir, tmp_dir_name = create_tmp_dir()
108108
# make a symlink to /etc/passwords in tmp_dir
109+
symlink = os.path.join(tmp_dir, 'symlink')
109110
os.symlink('/etc/passwd', os.path.join(tmp_dir, 'symlink'))
110111

111112
copy_compose_and_edit_directory('volume_load_symlinks_negative.yml', tmp_dir)
@@ -120,8 +121,8 @@ def test_symlinks_should_fail():
120121
container_running = check_if_container_running('test-container')
121122
runner.cleanup()
122123

123-
expected_error = 'trying to escape folder: /etc/passwd'
124-
assert expected_error in str(e.value), Tests.assertion_info(expected_error, str(e.value))
124+
expected_error = f"Service 'test-container' volume path ({symlink}) is outside allowed folder:"
125+
assert str(e.value).startswith(expected_error), Tests.assertion_info(expected_error, str(e.value))
125126
assert container_running is False, Tests.assertion_info('test-container stopped', 'test-container was still running!')
126127

127128
def test_non_bind_mounts_should_fail():

0 commit comments

Comments
 (0)