|
| 1 | +# OS System Calls |
| 2 | + |
| 3 | +Direct use of low-level operating system interfaces from the `os` module can be convenient, but it often bypasses Python’s safer, higher-level abstractions and introduces serious security risks. |
| 4 | + |
| 5 | +:::{danger} |
| 6 | +Be suspicious of direct `os.*` system calls in Python code. **Never trust — always verify.** |
| 7 | +::: |
| 8 | + |
| 9 | +The following Python `os` system calls should by default ring an alarm bell from a security point of view: |
| 10 | + |
| 11 | +* `os.system()` — executes a command in a subshell (equivalent to `subprocess` with `shell=True`). |
| 12 | +* The `os.exec*` family (`os.execl`, `os.execle`, `os.execlp`, `os.execlpe`, `os.execv`, `os.execve`, `os.execvp`, `os.execvpe`) — these replace the current process with a new program and **do not return**. |
| 13 | +* `os.fork()` — creates a child process (Unix only); can lead to fork bombs or resource exhaustion if misused. |
| 14 | +* `os.write()` / `os.writev()` — low-level writes to raw file descriptors. |
| 15 | +* And several other similar low-level functions. |
| 16 | + |
| 17 | +:::{tip} |
| 18 | +When shell-like functionality is required, prefer the modern `subprocess` module with proper safeguards (see the Subprocess section). Native Python APIs from `os`, `pathlib`, `shutil`, etc., are almost always safer and more portable. |
| 19 | +::: |
| 20 | + |
| 21 | +## Security Concerns |
| 22 | + |
| 23 | +The Python `os` functions above are powerful and easy to use, which makes them attractive — but also dangerous. |
| 24 | + |
| 25 | +**Key risks include:** |
| 26 | + |
| 27 | +- **Command injection** (especially with `os.system()` and similar shell-invoking calls) |
| 28 | +- **Process replacement** (the `exec*` family terminates the current Python interpreter) |
| 29 | +- **Fork bombs** and resource exhaustion via `os.fork()` |
| 30 | +- **File descriptor mismanagement** with `os.write()` / `os.writev()`, which can lead to data corruption, arbitrary file writes, information leakage, or denial of service |
| 31 | +- Privilege escalation if these calls run with elevated permissions |
| 32 | + |
| 33 | +These APIs operate at a very low level and provide minimal safety checks compared to higher-level Python constructs. |
| 34 | + |
| 35 | +## Preventive measures |
| 36 | + |
| 37 | +1. **Avoid `os.system()` entirely** — use `subprocess.run()` (or `check_*` variants) with a list of arguments and `shell=False`. |
| 38 | + |
| 39 | +2. **Prefer high-level Python APIs** |
| 40 | + Use `pathlib`, `shutil`, `os.makedirs()`, `open()`, etc., instead of shelling out for file and directory operations. |
| 41 | + |
| 42 | +3. **Never pass untrusted input** to any `os` function that executes commands or writes to file descriptors. |
| 43 | + |
| 44 | +4. **Validate all inputs rigorously** (paths, filenames, descriptors, etc.). Use allowlists and resolve paths with `pathlib.Path.resolve()`. |
| 45 | + |
| 46 | +5. **Restrict privileges** — run code with the minimum necessary permissions. Avoid running as root. |
| 47 | + |
| 48 | +6. **For `os.write()` / `os.writev()`**: |
| 49 | + - Only use known, validated file descriptors. |
| 50 | + - Prefer Python’s file objects (`open()` / `.write()`) which provide better safety and error handling. |
| 51 | + |
| 52 | +7. **Handle `os.fork()` with extreme care** (if unavoidable) — implement proper resource limiting and error handling to prevent fork bombs. |
| 53 | + |
| 54 | +8. **Consider sandboxing** or running untrusted code in isolated environments when executing system-level operations. |
| 55 | + |
| 56 | +## Example |
| 57 | + |
| 58 | +### Bad example (`os.system`) |
| 59 | + |
| 60 | +**Dangerous — shell injection possible:** |
| 61 | + |
| 62 | +```python |
| 63 | +import os |
| 64 | + |
| 65 | + |
| 66 | +os.system("rm -rf " + user_supplied_path) |
| 67 | +``` |
| 68 | + |
| 69 | +A simple **Attack scenario** can be: |
| 70 | +```python |
| 71 | +user_supplied_path = "/tmp/data; rm -rf /" |
| 72 | +``` |
| 73 | + |
| 74 | +### A bit better example |
| 75 | + |
| 76 | +Example of strict allow list-style validation. Not perfect but a bit more secure. |
| 77 | + |
| 78 | +```python |
| 79 | +from pathlib import Path |
| 80 | +import subprocess |
| 81 | + |
| 82 | +def safe_remove(path: str) -> None: |
| 83 | + """Safely remove a path after strict validation.""" |
| 84 | + target = Path(path).resolve() |
| 85 | + |
| 86 | + |
| 87 | + allowed_root = Path("/safe/directory").resolve() |
| 88 | + if not target.is_relative_to(allowed_root): |
| 89 | + raise ValueError("Path outside allowed directory") |
| 90 | + |
| 91 | + # Use modern subprocess with list arguments |
| 92 | + subprocess.run(["rm", "-rf", str(target)], check=True) |
| 93 | +``` |
| 94 | + |
| 95 | +Please never ever do a `rm` using a Python system call.You do not needed it, there are far better alternatives! |
| 96 | + |
| 97 | + |
| 98 | +### More secure example (pure Python, no `rm` or subprocess) |
| 99 | + |
| 100 | +```python |
| 101 | +from pathlib import Path |
| 102 | +import shutil |
| 103 | + |
| 104 | +def safe_remove(str, allowed_root= "/safe/directory"): |
| 105 | + """Safely remove a file or directory after strict validation. |
| 106 | + |
| 107 | + Uses pure Python standard library functions — no subprocess or shell calls. |
| 108 | + """ |
| 109 | + try: |
| 110 | + target = Path(path).resolve(strict=True) # strict=True raises if path doesn't exist |
| 111 | + root = Path(allowed_root).resolve(strict=True) |
| 112 | + |
| 113 | + # Strict containment check |
| 114 | + if not target.is_relative_to(root): |
| 115 | + raise ValueError(f"Path '{target}' is outside the allowed directory '{root}'") |
| 116 | + |
| 117 | + # Optional: additional explicit allowlist of permitted top-level directories |
| 118 | + # if target.parent != root: ... # further restrictions if needed |
| 119 | + |
| 120 | + if target.is_file() or target.is_symlink(): |
| 121 | + target.unlink(missing_ok=True) |
| 122 | + elif target.is_dir(): |
| 123 | + shutil.rmtree(target, ignore_errors=False) # do not ignore errors #nosec - Can be ignored by SAST scan |
| 124 | + else: |
| 125 | + raise ValueError(f"Path '{target}' is neither a file nor a directory") |
| 126 | + |
| 127 | + except Exception as e: |
| 128 | + raise RuntimeError(f"Failed to safely remove '{path}': {e}") from e |
| 129 | +``` |
| 130 | + |
| 131 | +This example is better and more secure: |
| 132 | + |
| 133 | +- **No subprocess or shell** — completely avoids `rm`, `os.system`, etc. |
| 134 | +- Uses `pathlib.Path` + `shutil.rmtree` — the idiomatic, safe Python way. |
| 135 | +- `resolve(strict=True)` ensures the path actually exists and resolves symlinks safely. |
| 136 | +- Clear exception handling with context. |
| 137 | +- `missing_ok=True` on files prevents unnecessary errors. |
| 138 | +- `ignore_errors=False` on `rmtree` ensures failures are not silently ignored. |
| 139 | +- Easy to extend with more validation (e.g. file type checks, size limits, etc.). |
| 140 | + |
| 141 | +:::{note} Note |
| 142 | + |
| 143 | +**Never use shell commands or low-level system calls when a safe native Python API exists.** |
| 144 | +::: |
| 145 | + |
| 146 | + |
| 147 | +## Discussion |
| 148 | + |
| 149 | +Low-level `os` calls are sometimes necessary for performance or very specific system interactions, but they should be treated as advanced and potentially **hazardous** features. |
| 150 | + |
| 151 | +In most applications, you can achieve the same goals more securely and portably using Python’s standard library abstractions. When process management or command execution is truly needed, the `subprocess` module (used correctly) is the recommended approach. |
| 152 | + |
| 153 | +Always assume that any direct system call may be misused and design your code with defence-in-depth principles. |
| 154 | + |
| 155 | +## More information |
| 156 | + |
| 157 | +* [Official `os` module documentation](https://docs.python.org/3/library/os.html) |
| 158 | +* [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html) |
| 159 | +* [Fork bomb / Rabbit virus attacks](https://www.imperva.com/learn/ddos/fork-bomb/) |
| 160 | + |
0 commit comments