Guide · AI and GenAI
Hydra's October 2026 Instantiate Vulnerability Explained: Config Is Code
17 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The Hydra instantiate vulnerability is a family of three advisories in hydra-core, a Python library that builds app settings from YAML files. Its instantiate() function imports and calls whatever a config's _target_ field names.
- So a config file is code. If an attacker controls the config, for example one shipped inside a downloaded model folder, they choose which Python function runs inside your process.
- Hydra 1.3.4 (4 July 2026) added a blocklist, a list of banned function names. Researchers bypassed it twice: through Python's timeit and pickle modules, then through PyYAML's unsafe loader. All three can run code hidden in text or data. Each round needed a new release.
- Upgrade the 1.3 line to 1.3.8 (25 October 2026), and never pass untrusted config to instantiate(). The fix that actually closes the gap is a narrow allowlist: only names your own code approves may run.
- Beginners can stop after the action steps. Engineers get a version-by-version table of which targets each release accepts, tested on real packages, plus a working allowlist check for 1.3.8 and the 1.4 whitelist API.
What goes wrong when a config file can choose which code runs?
The config stops being data and becomes a program, and whoever writes it can run code on your machine. That is the core of the Hydra instantiate vulnerability, fixed again on 25 October 2026.
Picture a common setup. A small machine learning team has a training tool that downloads a shared model folder from a public model hub. The folder holds weights plus a config.yaml, and the tool builds the model by passing that config to Hydra's instantiate(). The setup is made up, but the advisories describe exactly this pattern.
Everyone on the team thinks of config.yaml as settings: a learning rate, a layer count. Hydra reads one key in it, _target_, as the name of a Python function or class to import and call. Whoever wrote the folder decides that name.
We follow this one training tool through the guide. First the words, then the three advisories, then why each patch was bypassed, then what to do.
Which words do you need before reading the advisories?
- Hydra: an open-source Python library, published on PyPI as hydra-core, that builds an app's settings from YAML files and command-line overrides. The advisories' main example is loading machine learning models.
- YAML: a plain-text format for settings, with keys and values on separate lines. Hydra configs are YAML files.
- instantiate(): a Hydra function that reads a config block and creates the object it describes. Its sibling call() does the same thing under another name.
- _target_: the key inside a config block that holds a dotted Python path, such as myapp.Optimizer. Hydra imports that path and calls it.
- Remote code execution (RCE): an attacker making your program run code of their choice. Here it needs a victim to load the attacker's config.
- Blocklist: a list of names that are refused; everything else is allowed. Hydra's code calls it a blacklist.
- Allowlist: a list of names that are permitted; everything else is refused. Hydra 1.4 calls it an execution whitelist.
- GHSA: a GitHub Security Advisory ID, such as GHSA-wf6j-7hvr-gr93. CVE IDs are the global numbering for the same kind of record.
What is the Hydra instantiate vulnerability?
The Hydra instantiate vulnerability is the risk that untrusted config picks a dangerous callable through _target_. Three GitHub-reviewed advisories cover it, each rated High with a CVSS score of 7.8 out of 10.
The first advisory describes the pattern itself. The second and third each describe a way around the previous fix. Together they are a clean case study of why name blocklists struggle.
| Advisory | What it found | Affected | Fixed in |
|---|---|---|---|
| GHSA-2cp2-2r3c-7p7r (CVE-2026-68508) | instantiate() with untrusted config can run code; 1.3.4 adds a first blocklist | 1.3.3 and earlier | 1.3.4 |
| GHSA-rqx7-p7vv-w7hr (CVE-2026-106442) | Blocklist bypassed through timeit.timeit, pickle.loads, aliases and generic wrappers | 1.3.4 to 1.3.5; 1.4.0.dev0 to dev8 | 1.3.6 and 1.4.0.dev9 |
| GHSA-wf6j-7hvr-gr93 (no CVE yet) | Blocklist missed PyYAML's unsafe loaders, such as yaml.unsafe_load | 1.3.6 to 1.3.7; 1.4.0.dev9 to rc2 | 1.3.8 and 1.4.0rc3 |
Hydra is not a network service, so nobody can attack it directly over the internet. The CVSS vector says local access and user interaction: someone has to load the bad config. In our training tool, that someone is the engineer who downloads the model folder.
How does instantiate() turn a config into running code?
It reads the _target_ string, imports the module part, looks up the last part as an attribute, and calls it with the other keys as arguments. Hydra's own docs describe the lookup as a mix of import and getattr.
Here is a harmless module standing in for the training tool's code. The greet function prints a line, so you can see that loading config made code run.
# myapp.py: a harmless stand-in for your project's code
import importlib
class Optimizer:
def __init__(self, lr: float = 0.1):
self.lr = lr
def __repr__(self):
return f"Optimizer(lr={self.lr})"
def greet(name: str = "world"):
print(f"[side effect] greet() ran for {name}")
return f"hello {name}"
def call_by_name(module: str, name: str, arg):
# A generic dispatcher: the real function is data, not the _target_.
return getattr(importlib.import_module(module), name)(arg)Now a config that uses it, and the code that loads it. This is exactly what the training tool does with a downloaded config.yaml.
from hydra.utils import instantiate
from omegaconf import OmegaConf
cfg = OmegaConf.create("""
optimizer:
_target_: myapp.Optimizer
lr: 0.01
hello:
_target_: myapp.greet
name: config-file
""")
print(instantiate(cfg.optimizer))
print(instantiate(cfg.hello))Run it next to myapp.py on hydra-core 1.3.8. You should see three lines: Optimizer(lr=0.01), then the side-effect line from greet, then hello config-file. We got identical output on 1.3.3, before any fix.
What instantiate() does with one config block
Drag to exploreNotice that nothing between the YAML file and the call checks who wrote the file. On 1.3.3 every importable name reaches the last box; later versions add a name check in the middle.
Diagram connections:
- config.yaml from a folder to instantiate() reads block (loaded by your code)
- instantiate() reads block to Name check (1.3.4 and up) (_target_ string)
- Name check (1.3.4 and up) to import module part (not on the list)
- import module part to getattr last part
- getattr last part to Call with config args (callable found)
- config.yaml from a folder: Whoever wrote it chose _target_
- instantiate() reads block: Takes _target_ and the other keys
- Name check (1.3.4 and up): Refuses names on the blocklist
- import module part: e.g. import myapp
- getattr last part: e.g. myapp.greet
- Call with config args: Runs in your process, with your rights
The call runs with everything your process can touch: files, environment variables, cloud credentials, the GPU box itself. The first advisory lists reading credentials and changing files as possible effects.
Why did the Hydra blocklist fixes keep failing?
Because Python has far more ways to run code than any list can name. Each fix banned the routes people had reported, and the next report used a route nobody had listed.
An analogy helps. A blocklist is a guard with a list of banned visitors; anyone not on the list walks in. An allowlist is a guest list; anyone not on it waits outside. Hydra's targets are the visitors, and the list lives in Hydra's code.
Where the picture breaks: real visitors cannot change their names, but one Python function can be reached under many dotted names. That makes the banned-visitor list even weaker than the analogy suggests.
Round one: 1.3.4 bans the obvious names
Hydra 1.3.4, published to PyPI on 4 July 2026, added a set of 64 exact names plus two prefixes, os.exec and os.spawn. It covered names like os.system, subprocess.run, builtins.eval and importlib.import_module.
The check compared the _target_ string with the list, after mapping posix and nt to os. That is a string test, not a test of what the name finally points to.
Round two: 1.3.6 bans wrappers and deserialisers
The second advisory showed that code can run through functions that are not obviously dangerous. Python's timeit.timeit times a statement given as text, so it executes that text.
Python's own pickle docs warn that malicious pickle data can execute arbitrary code during unpickling, so pickle.loads was another route. The advisory also lists aliases and generic dispatch helpers that hide the real callable.
Hydra 1.3.6, published 29 August 2026, grew the list and started checking the resolved function, not just the string. In our test it reports posix.system as os.system, resolved from posix.system.
Round three: 1.3.8 bans PyYAML's unsafe loaders
The third advisory found that yaml.unsafe_load was not on the list. PyYAML's unsafe loader can build Python objects from tags inside the YAML text, so a config could pass it a stream that runs code.
Hydra 1.3.8 bans every target in the yaml module except four functions: safe_load, safe_load_all, safe_dump and safe_dump_all. The advisory notes PyYAML 6.0.3's full_load rejected the demonstrated payload, so not every loader is claimed exploitable.
The route no list can see: your own code
The 1.3.8 source file says it plainly. Trusted installed callables can indirectly dispatch operations that never appear as _target_ values. Our call_by_name helper is such a callable.
With _target_ set to myapp.call_by_name, the real function is chosen by two ordinary config values, module and name. We ran it on 1.3.8 with math and sqrt, and it returned 4.0. The blocklist only ever saw myapp.call_by_name.
| _target_ | 1.3.3 | 1.3.4 | 1.3.6 | 1.3.8 | 1.4.0rc3, no whitelist |
|---|---|---|---|---|---|
| math.sqrt | Accepted | Accepted | Accepted | Accepted | Accepted |
| myapp.call_by_name | Accepted | Accepted | Accepted | Accepted | Accepted |
| os.system | Accepted | Refused | Refused | Refused | Refused |
| posix.system (alias) | Accepted | Refused | Refused | Refused | Refused |
| timeit.timeit | Accepted | Accepted | Refused | Refused | Refused |
| pickle.loads | Accepted | Accepted | Refused | Refused | Refused |
| yaml.unsafe_load | Accepted | Accepted | Accepted | Refused | Refused |
| yaml.safe_load | Accepted | Accepted | Accepted | Accepted | Accepted |
Read the table from left to right and you can watch the list chase the reports. The two main banned-name sets grew from 64 names in 1.3.4 to 187 in 1.3.6 and 225 in 1.3.8. In 1.3.8, smaller sets of dispatch and wrapper helpers and 12 name prefixes come on top. Our own dispatcher passes every version.
Why does an allowlist win where a blocklist loses?
An allowlist only has to name what your app needs, which is a short list you already know. A blocklist has to name everything dangerous in Python and every installed library, which nobody can know.
For our training tool the needed list might be three or four names: the model class, the optimiser and the data loader. Every other name, including ones invented next year, is refused by default.
Blocklist vs allowlist for one _target_
Drag to exploreNotice where a new, unknown name ends up. The blocklist lets it run by default; the allowlist refuses it by default.
Diagram connections:
- _target_ from config to On Hydra's banned list? (1.3.x path)
- _target_ from config to On your approved list? (allowlist path)
- On Hydra's banned list? to Runs (No)
- On Hydra's banned list? to Refused (Yes)
- On your approved list? to Runs, as you intended (Yes)
- On your approved list? to Refused (No)
- _target_ from config: Any dotted Python name
- On Hydra's banned list?: 1.3.8: 225+ names, 12 prefixes
- On your approved list?: 1.4 whitelist, or your own check
- Runs: Unknown names run by default
- Refused: InstantiationException or your error
- Runs, as you intended: Only names you chose
An allowlist is only as good as its entries. If you approve a generic dispatcher like call_by_name, you have approved everything it can reach. Keep entries to concrete classes and functions that do one known job.
Which Hydra versions are affected and who needs to act?
Every hydra-core release up to and including 1.3.7 is affected by at least one advisory, and so is every 1.4 pre-release before 1.4.0rc3. You need to act if your app passes config you did not write to instantiate() or call().
- Act now: tools that load configs from downloaded models, shared experiment folders, user uploads, pull requests or job queues, then call instantiate() on them.
- Act now: services that accept command-line overrides or model metadata from users and feed them into instantiate().
- Lower risk: an app that only instantiates config files written by your own team and stored in your own repository. The first advisory says trusted, app-owned config is not affected in the same way.
- Still upgrade: libraries built on Hydra may load configs for you. Check whether any dependency calls instantiate() on files you pass in.
Even on 1.3.8, the advisories say the 1.3 blocklist is not a complete security boundary. Upgrading removes the known routes; it does not make untrusted config safe.
What should you do now about the Hydra advisories?
Find where config reaches instantiate()
Search your code for instantiate( and call( from hydra.utils. For each one, write down where the config comes from.
Upgrade hydra-core to 1.3.8
Pin hydra-core==1.3.8 in your requirements file and reinstall. As of 28 October 2026 it is the latest stable release.
Stop passing untrusted config
Do not instantiate configs from downloads, uploads or other people's folders. Read plain values from them instead, and choose classes in your own code.
Add your own allowlist where you must accept outside config
Before calling instantiate(), check every _target_ against a fixed set of names written in Python. The engineer section shows a tested version.
Plan for the 1.4 whitelist
Hydra 1.4 is a release candidate today. Try it in a test branch, but keep production on the stable 1.3 line until 1.4.0 is final.
How do you check your Hydra version and blocklist are working?
Print the installed version first. Then ask instantiate() to resolve a banned name with _partial_: true, which returns a wrapped function without calling it.
import functools
import hydra
from hydra.utils import instantiate
from omegaconf import OmegaConf
print("hydra-core", hydra.__version__)
for target in ["yaml.safe_load", "yaml.unsafe_load"]:
cfg = OmegaConf.create({"_target_": target, "_partial_": True})
try:
result = instantiate(cfg)
assert isinstance(result, functools.partial)
print(target, "accepted (not called)")
except Exception as e:
print(target, "refused:", type(e).__name__)On 1.3.8 you should see hydra-core 1.3.8, then yaml.safe_load accepted and yaml.unsafe_load refused with InstantiationException. On 1.3.7, and on 1.3.3, 1.3.4 and 1.3.6 in our tests, both lines say accepted, and you still need the upgrade.
A note on the 1.3 escape hatch: the environment variable HYDRA_INSTANTIATE_ALLOWLIST_OVERRIDE lets trusted operators unblock some names. In our 1.3.8 test it unblocked os.remove but not os.system, which the source marks as never overridable. We found this split in the 1.3.8 source, not the release notes, as of 28 October 2026; pin your version if you rely on it.
How do you add an allowlist to Hydra 1.3.8 today?
Write the approved names in Python, resolve the config, collect every _target_ in it and refuse the whole config if any name is missing from your set. Only then call instantiate().
from hydra.utils import instantiate
from omegaconf import OmegaConf
# Written in code, never read from the config being checked.
ALLOWED_TARGETS = frozenset({"myapp.Optimizer"})
def find_targets(node):
if isinstance(node, dict):
if "_target_" in node:
yield node["_target_"]
for value in node.values():
yield from find_targets(value)
elif isinstance(node, list):
for item in node:
yield from find_targets(item)
def safe_instantiate(cfg):
plain = OmegaConf.to_container(cfg, resolve=True) # expand ${...} first
bad = [t for t in find_targets(plain) if t not in ALLOWED_TARGETS]
if bad:
raise ValueError(f"refused targets: {bad}")
return instantiate(cfg)We tested it on 1.3.8 with four configs. A plain myapp.Optimizer block built Optimizer(lr=0.01). A top-level myapp.greet was refused, and so was math.sqrt hidden in a list inside an allowed block.
The fourth case hid myapp.greet behind an interpolation, a ${t} reference to another key. Resolving first caught it, and the check refused it.
- Exact matches only: comparing whole strings means an alias such as a re-exported name is refused unless you list it yourself.
- Scope: it checks _target_ keys only. Custom OmegaConf resolvers or your own code that imports names from config values are outside it.
- Pass everything through it: if any code path calls instantiate() directly on outside config, the check does nothing for that path.
What is coming in Hydra 1.4's execution whitelist?
Hydra 1.4 makes the allowlist the main boundary, under the name execution whitelist. It is in 1.4.0rc3, a release candidate from 25 October 2026, so details may still change before 1.4.0.
You pass the list from trusted Python code, either to instantiate() as _execution_whitelist_, through the execution_whitelist() context manager, or to @hydra.main(execution_whitelist=...).
from hydra.utils import instantiate
from omegaconf import OmegaConf
cfg = OmegaConf.create({"_target_": "myapp.Optimizer", "lr": 0.01})
print(instantiate(cfg, _execution_whitelist_=["myapp.Optimizer"]))
bad = OmegaConf.create({"_target_": "myapp.greet"})
instantiate(bad, _execution_whitelist_=["myapp.Optimizer"]) # raises InstantiationExceptionWe ran these cases on hydra-core 1.4.0rc3 with OmegaConf 2.4.1rc1 on 28 October 2026:
- With no whitelist, instantiate() still works but emits a Python warning that this will become an error in Hydra 1.5.
- With ["myapp.Optimizer"], the Optimizer block builds and myapp.greet is refused as not in the Hydra execution whitelist.
- A config that sets _execution_whitelist_ inside itself is refused: the list must come from trusted code.
- A bare "*" entry is refused. Only trailing package wildcards such as myapp.* are accepted.
- Listing os.system does not help: Hydra says that target cannot be authorised by the whitelist.
- UNSAFE_DISABLE_EXECUTION_CHECKS restores the old allow-everything behaviour, and its name says what it is.
Two cautions from the docs and our tests. A prefix such as myapp.* also covers modules other installed packages add under that name. And our myapp.* run let call_by_name reach math.sqrt, so a wildcard can approve a dispatcher by accident.
How do you explain the Hydra instantiate vulnerability in a system design interview?
Treat it as a trust-boundary question: who controls the input that chooses which code runs? Use this frame.
Name the boundary
Config from outside our team crosses into a process with credentials. Anything that turns that config into code needs a gate.
Explain the mechanism
Hydra's instantiate() imports and calls the dotted name in _target_, so control of the file means control of the call.
Compare blocklist and allowlist
Blocklists fail open on unknown names and needed three releases. Allowlists fail closed and only need the names our app uses.
Place the list correctly
The allowed names live in code we review, never in the config they guard, and they avoid generic dispatchers.
Add depth
Run untrusted loading in a container without secrets, prefer safe formats like JSON or safe YAML loaders, and pin library versions.
Questions people ask
- Is the Hydra instantiate vulnerability a bug I can be hit by over the network?
- Not directly. Hydra is a library, not a server, and the advisories rate the attack vector as local with user interaction. The danger comes when your app loads someone else's config, such as one inside a downloaded model folder, and passes it to instantiate(). A web service that feeds user input into instantiate() would expose it remotely through your own code.
- Does upgrading to hydra-core 1.3.8 make untrusted configs safe?
- No. Version 1.3.8 blocks the routes reported so far, including yaml.unsafe_load. Both later advisories say the 1.3 blocklist is best-effort and not a complete security boundary. Upgrade, and also stop passing untrusted config to instantiate(), or check every _target_ against your own allowlist first.
- Why not just add the missing names to the blocklist myself?
- Because you would be repeating the race the Hydra maintainers lost twice. Python and its libraries offer many ways to run code, through wrappers, deserialisers, aliases and dispatch helpers. Your own project code can add more. A short list of names you approve is easier to get right and stays right when new routes appear.
- Should I move production to Hydra 1.4 now?
- Not yet, as of 28 October 2026. The newest 1.4 build is 1.4.0rc3, a release candidate, and the docs label 1.4 the same way. Test the execution whitelist in a branch so you are ready, and keep production on 1.3.8 with your own allowlist check until 1.4.0 is final.
- Is yaml.safe_load still allowed as a _target_ on 1.3.8?
- Yes. Hydra 1.3.8 blocks every target in the yaml module except four: safe_load, safe_load_all, safe_dump and safe_dump_all. We confirmed yaml.safe_load is accepted and yaml.unsafe_load and yaml.load are refused on 1.3.8, using _partial_ so nothing was called.
- Does the 1.4 whitelist cover Hydra's logging setup too?
- Yes, according to the 1.4 docs. Hydra's logging setup can import handler classes named in config, so custom handlers must be on the whitelist. The docs add that the whitelist does not protect calls your own code makes to Python's logging configuration functions directly.
Sources and further reading
- GitHub Advisory Database: GHSA-wf6j-7hvr-gr93, legacy target blacklist permits unsafe PyYAML loading (27 October 2026)
- GitHub Advisory Database: GHSA-rqx7-p7vv-w7hr (CVE-2026-106442), blacklist bypasses permit code execution
- GitHub Advisory Database: GHSA-2cp2-2r3c-7p7r (CVE-2026-68508), instantiate with untrusted config
- Hydra 1.3.4 release notes: first blocklist
- Hydra 1.3.6 release notes: hardened authorisation
- Hydra 1.3.8 release notes: unsafe PyYAML loaders blocked
- Hydra 1.4.0rc3 pre-release notes
- Hydra docs (1.4 release candidate): execution whitelist
- Hydra docs (1.4 release candidate): instantiating objects overview
- PyPI: hydra-core release history
- Python docs: pickle security warning
- Python docs: timeit