Validate cluster_manager against an allowlist before passing to jl.seval
## Summary
`PySRRegressor(cluster_manager=<str>)` is documented as a backend selector (`"slurm"`, `"sge"`, …), but the string is interpolated into a Julia `seval(...)` with no validation. A surprising value — from a typo, a config file, or a submission-script wrapper — becomes arbitrary Julia execution.
## Where
`pysr/julia_helpers.py`:
```python
def _load_cluster_manager(cluster_manager: str):
jl.seval(f"using ClusterManagers: addprocs_{cluster_manager}")
return jl.seval(f"addprocs_{cluster_manager}")
```
Called from `pysr/sr.py` (the `cluster_manager = _load_cluster_manager(cluster_manager)` line inside the `fit` path).
## Repro
No Julia startup needed — we just watch what `jl.seval` receives:
```python
import ast, pathlib
src = pathlib.Path("pysr/julia_helpers.py").read_text()
func = next(
n for n in ast.parse(src).body
if isinstance(n, ast.FunctionDef) and n.name == "_load_cluster_manager"
)
class FakeJL:
def seval(self, s):
print(f"seval({s!r})")
return lambda *a, **k: None
ns = {"jl": FakeJL()}
exec(compile(ast.Module(body=[func], type_ignores=[]), "x", "exec"), ns)
ns["_load_cluster_manager"]('slurm\nerror("pwned")')
```
Output:
```
seval('using ClusterManagers: addprocs_slurm\nerror("pwned")')
seval('addprocs_slurm\nerror("pwned")')
```
The attacker-controlled bytes reach the Julia evaluator verbatim. Any non-allowlisted string — `""`, `"; rm -rf /"`, `"$(whoami)"`, `"not_a_real_backend"` — is accepted without a pre-flight check.
## Why this matters even though custom Julia code is a feature
`loss_function`, `elementwise_loss`, etc. are *documented* as user-supplied Julia code channels — that's intentional. `cluster_manager` reads to users as a named backend selector, not as a code channel. With #794 adding more backends reachable through this path, the surface is growing rather than shrinking.
## Suggested fix
```python
_KNOWN_CLUSTER_MANAGERS = frozenset(
{"slurm", "sge", "pbs", "qsub", "lsf", "htc", "htcondor", "torque"}
)
def _load_cluster_manager(cluster_manager: str):
if cluster_manager not in _KNOWN_CLUSTER_MANAGERS:
raise ValueError(
f"Unknown cluster_manager {cluster_manager!r}; "
f"expected one of {sorted(_KNOWN_CLUSTER_MANAGERS)}"
)
jl.seval(f"using ClusterManagers: addprocs_{cluster_manager}")
return jl.seval(f"addprocs_{cluster_manager}")
```
Happy to open a small PR once the allowlist shape is agreed on (e.g., whether to include the newer `slurm_native` variant from #794, or dispatch via a dict instead).
0 条评论