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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chorut
3
- Version: 0.1.4
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 require explicit bash
307
- result = chroot.execute("bash -c 'ls | wc -l'", capture_output=True)
308
- line_count = int(result.stdout.strip())
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 require explicit bash
274
- result = chroot.execute("bash -c 'ls | wc -l'", capture_output=True)
275
- line_count = int(result.stdout.strip())
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 '{target_rel}'")
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([f"'{source}'", f"'{target_rel}'"])
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(f"'{arg}'" for arg in command)
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
- with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
621
- f.write(script_content)
622
- script_path = f.name
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "chorut"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Python implementation of an enhanced chroot functionality with minimal dependencies"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes