Strict allowlist validation for dev server commands. Prevents arbitrary command execution (no sh -c, no cmd /c, no python -c, etc.)
(cmd: str)
| 89 | |
| 90 | |
| 91 | def validate_custom_command_strict(cmd: str) -> None: |
| 92 | """ |
| 93 | Strict allowlist validation for dev server commands. |
| 94 | Prevents arbitrary command execution (no sh -c, no cmd /c, no python -c, etc.) |
| 95 | """ |
| 96 | if not isinstance(cmd, str) or not cmd.strip(): |
| 97 | raise ValueError("custom_command cannot be empty") |
| 98 | |
| 99 | argv = shlex.split(cmd, posix=(sys.platform != "win32")) |
| 100 | if not argv: |
| 101 | raise ValueError("custom_command could not be parsed") |
| 102 | |
| 103 | base = Path(argv[0]).name.lower() |
| 104 | |
| 105 | # Block direct shells / interpreters commonly used for command injection |
| 106 | if base in BLOCKED_SHELLS: |
| 107 | raise ValueError(f"custom_command runner not allowed: {base}") |
| 108 | |
| 109 | if base not in ALLOWED_RUNNERS: |
| 110 | raise ValueError( |
| 111 | f"custom_command runner not allowed: {base}. " |
| 112 | f"Allowed: {', '.join(sorted(ALLOWED_RUNNERS))}" |
| 113 | ) |
| 114 | |
| 115 | # Block one-liner execution for python |
| 116 | lowered = [a.lower() for a in argv] |
| 117 | if base in {"python", "python3"}: |
| 118 | if "-c" in lowered: |
| 119 | raise ValueError("python -c is not allowed") |
| 120 | if len(argv) >= 3 and argv[1] == "-m": |
| 121 | # Allow: python -m <allowed_module> ... |
| 122 | if argv[2] not in ALLOWED_PYTHON_MODULES: |
| 123 | raise ValueError( |
| 124 | f"python -m {argv[2]} is not allowed. " |
| 125 | f"Allowed modules: {', '.join(sorted(ALLOWED_PYTHON_MODULES))}" |
| 126 | ) |
| 127 | elif len(argv) >= 2 and argv[1].endswith(".py"): |
| 128 | # Allow: python manage.py runserver, python app.py, etc. |
| 129 | pass |
| 130 | else: |
| 131 | raise ValueError( |
| 132 | "Python commands must use 'python -m <module> ...' or 'python <script>.py ...'" |
| 133 | ) |
| 134 | |
| 135 | if base == "flask": |
| 136 | # Allow: flask run [--host ...] [--port ...] |
| 137 | if len(argv) < 2 or argv[1] != "run": |
| 138 | raise ValueError("flask custom_command must be 'flask run [options]'") |
| 139 | |
| 140 | if base == "poetry": |
| 141 | # Allow: poetry run <subcmd> ... |
| 142 | if len(argv) < 3 or argv[1] != "run": |
| 143 | raise ValueError("poetry custom_command must be 'poetry run <command> ...'") |
| 144 | |
| 145 | if base == "uvicorn": |
| 146 | if len(argv) < 2 or ":" not in argv[1]: |
| 147 | raise ValueError("uvicorn must specify an app like module:app") |
| 148 |
no outgoing calls