chorut 0.1.4__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chorut-0.1.4 → chorut-0.1.5}/PKG-INFO +29 -6
- {chorut-0.1.4 → chorut-0.1.5}/README.md +28 -5
- {chorut-0.1.4 → chorut-0.1.5}/chorut/__init__.py +20 -16
- {chorut-0.1.4 → chorut-0.1.5}/pyproject.toml +1 -1
- {chorut-0.1.4 → chorut-0.1.5}/.gitignore +0 -0
- {chorut-0.1.4 → chorut-0.1.5}/LICENSE +0 -0
- {chorut-0.1.4 → chorut-0.1.5}/chorut/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chorut
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Python implementation of an enhanced chroot functionality with minimal dependencies
|
|
5
5
|
Project-URL: Homepage, https://github.com/abuss/chorut
|
|
6
6
|
Project-URL: Repository, https://github.com/abuss/chorut.git
|
|
@@ -293,7 +293,7 @@ Returns a `subprocess.CompletedProcess` object with:
|
|
|
293
293
|
##### execute() Examples
|
|
294
294
|
|
|
295
295
|
```python
|
|
296
|
-
# List command
|
|
296
|
+
# List command (recommended for security)
|
|
297
297
|
result = chroot.execute(['ls', '-la'])
|
|
298
298
|
|
|
299
299
|
# String command
|
|
@@ -303,17 +303,40 @@ result = chroot.execute('ls -la')
|
|
|
303
303
|
result = chroot.execute('cat /etc/hostname', capture_output=True)
|
|
304
304
|
hostname = result.stdout.strip()
|
|
305
305
|
|
|
306
|
-
# Shell features
|
|
307
|
-
result = chroot.execute(
|
|
308
|
-
|
|
306
|
+
# Shell features work automatically with auto_shell=True (default)
|
|
307
|
+
result = chroot.execute('ls | wc -l', capture_output=True) # Pipes work!
|
|
308
|
+
result = chroot.execute('echo hello && echo world') # Logical operators
|
|
309
|
+
result = chroot.execute('ls *.txt') # Glob patterns
|
|
310
|
+
|
|
311
|
+
# List commands with special characters are handled safely
|
|
312
|
+
result = chroot.execute(['echo', 'hello world', 'foo;bar'])
|
|
309
313
|
|
|
310
314
|
# Interactive shell (command=None)
|
|
311
315
|
chroot.execute() # Starts bash shell
|
|
312
316
|
```
|
|
313
317
|
|
|
318
|
+
### Command List Validation
|
|
319
|
+
|
|
320
|
+
When using list-based commands, all arguments are validated:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
# Valid - all arguments are strings
|
|
324
|
+
result = chroot.execute(['echo', 'hello', 'world'])
|
|
325
|
+
|
|
326
|
+
# Raises ChrootError - non-string arguments
|
|
327
|
+
result = chroot.execute(['echo', 123]) # Error: All command arguments must be strings
|
|
328
|
+
|
|
329
|
+
# Raises ChrootError - empty list
|
|
330
|
+
result = chroot.execute([]) # Error: Command list cannot be empty
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Security
|
|
334
|
+
|
|
335
|
+
All user-provided values (mount sources, targets, command arguments) are properly escaped using `shlex.quote()` to prevent command injection attacks. List-based commands are recommended over string commands when handling untrusted input.
|
|
336
|
+
|
|
314
337
|
### Exceptions
|
|
315
338
|
|
|
316
|
-
- `ChrootError`: Raised for chroot-related errors
|
|
339
|
+
- `ChrootError`: Raised for chroot-related errors, invalid command arguments, or empty command lists
|
|
317
340
|
- `MountError`: Raised for mount-related errors
|
|
318
341
|
|
|
319
342
|
## Requirements
|
|
@@ -260,7 +260,7 @@ Returns a `subprocess.CompletedProcess` object with:
|
|
|
260
260
|
##### execute() Examples
|
|
261
261
|
|
|
262
262
|
```python
|
|
263
|
-
# List command
|
|
263
|
+
# List command (recommended for security)
|
|
264
264
|
result = chroot.execute(['ls', '-la'])
|
|
265
265
|
|
|
266
266
|
# String command
|
|
@@ -270,17 +270,40 @@ result = chroot.execute('ls -la')
|
|
|
270
270
|
result = chroot.execute('cat /etc/hostname', capture_output=True)
|
|
271
271
|
hostname = result.stdout.strip()
|
|
272
272
|
|
|
273
|
-
# Shell features
|
|
274
|
-
result = chroot.execute(
|
|
275
|
-
|
|
273
|
+
# Shell features work automatically with auto_shell=True (default)
|
|
274
|
+
result = chroot.execute('ls | wc -l', capture_output=True) # Pipes work!
|
|
275
|
+
result = chroot.execute('echo hello && echo world') # Logical operators
|
|
276
|
+
result = chroot.execute('ls *.txt') # Glob patterns
|
|
277
|
+
|
|
278
|
+
# List commands with special characters are handled safely
|
|
279
|
+
result = chroot.execute(['echo', 'hello world', 'foo;bar'])
|
|
276
280
|
|
|
277
281
|
# Interactive shell (command=None)
|
|
278
282
|
chroot.execute() # Starts bash shell
|
|
279
283
|
```
|
|
280
284
|
|
|
285
|
+
### Command List Validation
|
|
286
|
+
|
|
287
|
+
When using list-based commands, all arguments are validated:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Valid - all arguments are strings
|
|
291
|
+
result = chroot.execute(['echo', 'hello', 'world'])
|
|
292
|
+
|
|
293
|
+
# Raises ChrootError - non-string arguments
|
|
294
|
+
result = chroot.execute(['echo', 123]) # Error: All command arguments must be strings
|
|
295
|
+
|
|
296
|
+
# Raises ChrootError - empty list
|
|
297
|
+
result = chroot.execute([]) # Error: Command list cannot be empty
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Security
|
|
301
|
+
|
|
302
|
+
All user-provided values (mount sources, targets, command arguments) are properly escaped using `shlex.quote()` to prevent command injection attacks. List-based commands are recommended over string commands when handling untrusted input.
|
|
303
|
+
|
|
281
304
|
### Exceptions
|
|
282
305
|
|
|
283
|
-
- `ChrootError`: Raised for chroot-related errors
|
|
306
|
+
- `ChrootError`: Raised for chroot-related errors, invalid command arguments, or empty command lists
|
|
284
307
|
- `MountError`: Raised for mount-related errors
|
|
285
308
|
|
|
286
309
|
## Requirements
|
|
@@ -8,6 +8,8 @@ using only Python standard library modules.
|
|
|
8
8
|
import contextlib
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
11
13
|
import subprocess
|
|
12
14
|
import sys
|
|
13
15
|
from pathlib import Path
|
|
@@ -279,8 +281,6 @@ class ChrootManager:
|
|
|
279
281
|
Returns True if the command contains shell features like pipes, redirects,
|
|
280
282
|
command substitution, logical operators, etc.
|
|
281
283
|
"""
|
|
282
|
-
import re
|
|
283
|
-
|
|
284
284
|
# Shell metacharacters that require shell interpretation
|
|
285
285
|
shell_patterns = [
|
|
286
286
|
r"\|", # Pipes: cmd1 | cmd2
|
|
@@ -505,10 +505,10 @@ class ChrootManager:
|
|
|
505
505
|
mkdir = mount_spec.get("mkdir", True)
|
|
506
506
|
|
|
507
507
|
if verbose:
|
|
508
|
-
script_lines.append(f"echo 'Mounting {source} -> {target_rel}'")
|
|
508
|
+
script_lines.append(f"echo 'Mounting {shlex.quote(source)} -> {shlex.quote(target_rel)}'")
|
|
509
509
|
|
|
510
510
|
if mkdir:
|
|
511
|
-
script_lines.append(f"mkdir -p
|
|
511
|
+
script_lines.append(f"mkdir -p {shlex.quote(target_rel)}")
|
|
512
512
|
|
|
513
513
|
mount_cmd = ["mount"]
|
|
514
514
|
if bind:
|
|
@@ -517,9 +517,9 @@ class ChrootManager:
|
|
|
517
517
|
mount_cmd.extend(["-t", fstype])
|
|
518
518
|
|
|
519
519
|
if options:
|
|
520
|
-
mount_cmd.extend(["-o", options])
|
|
520
|
+
mount_cmd.extend(["-o", shlex.quote(options)])
|
|
521
521
|
|
|
522
|
-
mount_cmd.extend([
|
|
522
|
+
mount_cmd.extend([shlex.quote(source), shlex.quote(target_rel)])
|
|
523
523
|
script_lines.append(" ".join(mount_cmd))
|
|
524
524
|
|
|
525
525
|
script_lines.extend(
|
|
@@ -535,9 +535,9 @@ class ChrootManager:
|
|
|
535
535
|
|
|
536
536
|
chroot_cmd = ["chroot"]
|
|
537
537
|
if userspec:
|
|
538
|
-
chroot_cmd.extend(["--userspec", userspec])
|
|
538
|
+
chroot_cmd.extend(["--userspec", shlex.quote(userspec)])
|
|
539
539
|
chroot_cmd.append(".")
|
|
540
|
-
chroot_cmd.extend(
|
|
540
|
+
chroot_cmd.extend(shlex.quote(arg) for arg in command)
|
|
541
541
|
|
|
542
542
|
script_lines.append(" ".join(chroot_cmd))
|
|
543
543
|
|
|
@@ -600,14 +600,18 @@ class ChrootManager:
|
|
|
600
600
|
if command is None:
|
|
601
601
|
command = ["/bin/bash"]
|
|
602
602
|
elif isinstance(command, str):
|
|
603
|
-
import shlex
|
|
604
|
-
|
|
605
603
|
# Auto-detect shell features and wrap with bash -c if needed
|
|
606
604
|
if self.auto_shell and self._needs_shell(command):
|
|
607
605
|
logger.debug(f"Auto-detected shell features in command: {command}")
|
|
608
606
|
command = ["bash", "-c", command]
|
|
609
607
|
else:
|
|
610
608
|
command = shlex.split(command)
|
|
609
|
+
elif isinstance(command, list):
|
|
610
|
+
# Validate that all elements in the list are strings
|
|
611
|
+
if not all(isinstance(arg, str) for arg in command):
|
|
612
|
+
raise ChrootError("All command arguments must be strings")
|
|
613
|
+
if len(command) == 0:
|
|
614
|
+
raise ChrootError("Command list cannot be empty")
|
|
611
615
|
|
|
612
616
|
if self.unshare_mode:
|
|
613
617
|
# For unshare mode, create a script and run it in unshared namespace
|
|
@@ -617,18 +621,18 @@ class ChrootManager:
|
|
|
617
621
|
# Write script to a temporary file
|
|
618
622
|
import tempfile
|
|
619
623
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
624
|
+
fd, script_path = tempfile.mkstemp(suffix=".sh", text=True)
|
|
625
|
+
try:
|
|
626
|
+
os.write(fd, script_content.encode())
|
|
627
|
+
finally:
|
|
628
|
+
os.close(fd)
|
|
629
|
+
os.chmod(script_path, 0o700)
|
|
623
630
|
|
|
624
631
|
logger.debug("Unshare script written to: %s", script_path)
|
|
625
632
|
if logger.isEnabledFor(logging.DEBUG):
|
|
626
633
|
logger.debug("Script content:\n%s", script_content)
|
|
627
634
|
|
|
628
635
|
try:
|
|
629
|
-
# Make script executable
|
|
630
|
-
os.chmod(script_path, 0o755)
|
|
631
|
-
|
|
632
636
|
# Run the script in unshared namespace
|
|
633
637
|
unshare_cmd = ["unshare", "--fork", "--pid", "--mount", "--map-auto", "--map-root-user", script_path]
|
|
634
638
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|