Summary
BasicSR loads its YAML configs through a loader that is not a safe loader. ordered_yaml()
uses the full PyYAML loader to get OrderedDict support, and both branches it can take are
unsafe. Loading a crafted config therefore executes arbitrary code in the process.
Location
# basicsr/utils/options.py:13-29
def ordered_yaml():
"""Support OrderedDict for yaml."""
try:
from yaml import CDumper as Dumper
from yaml import CLoader as Loader # <- full loader, unsafe
except ImportError:
from yaml import Dumper, Loader # <- full loader, unsafe
...
Loader.add_constructor(_mapping_tag, dict_constructor)
return Loader, Dumper
# basicsr/utils/options.py:47-51
def yaml_load(f):
if os.path.isfile(f):
with open(f, 'r') as f:
return yaml.load(f, Loader=ordered_yaml()[0])
else:
return yaml.load(f, Loader=ordered_yaml()[0])
basicsr/utils/options.py:111 (opt = yaml_load(args.opt)) is the entry point everyone hits:
train.py, test.py, etc.
Root cause
Neither branch is a safe loader.
yaml.CLoader (the preferred branch when libyaml is available) is the full loader;
yaml.Loader (the ImportError fallback) is the full loader.
Both support tags such as !!python/object/apply, which let yaml.load invoke arbitrary Python
callables during parsing. PyYAML's own docs say to use safe_load / SafeLoader for untrusted input.
Note the OrderedDict requirement does not need the full loader — a SafeLoader with a custom
mapping constructor does the same job (see the fix below).
Steps to reproduce
import os, tempfile, yaml
p = tempfile.mktemp(suffix=".yml")
with open(p, "w") as f:
f.write('!!python/object/apply:os.system ["touch /tmp/BSR_MARK"]\n')
# fallback branch (what ordered_yaml() returns without libyaml)
with open(p) as f:
yaml.load(f, Loader=yaml.Loader) # -> executes os.system
# preferred branch (what it returns *with* libyaml) is also unsafe:
yaml.load('!!python/object/apply:os.system ["true"]', Loader=yaml.CLoader)
Observed:
yaml.Loader fallback -> marker exists: True
CLoader: loaded (also unsafe)
Expected results
Parsing a config should build plain data structures only; !!python/... tags should raise an error
instead of executing anything (i.e. SafeLoader behavior).
Actual results
A crafted config executes arbitrary commands in the loading process.
Impact
BasicSR configs are routinely shared, copied and downloaded (the project ships many
options/*.yml templates and users exchange them freely). Anyone who can get a user to load a
crafted config — or who can tamper with a config they fetch — gets arbitrary code execution at that
process's privilege level.
A local, self-authored config is not a trust boundary on its own, so exploitability depends on the
config's provenance, which in practice is often third-party.
Summary
BasicSR loads its YAML configs through a loader that is not a safe loader.
ordered_yaml()uses the full PyYAML loader to get
OrderedDictsupport, and both branches it can take areunsafe. Loading a crafted config therefore executes arbitrary code in the process.
Location
basicsr/utils/options.py:111(opt = yaml_load(args.opt)) is the entry point everyone hits:train.py,test.py, etc.Root cause
Neither branch is a safe loader.
yaml.CLoader(the preferred branch when libyaml is available) is the full loader;yaml.Loader(theImportErrorfallback) is the full loader.Both support tags such as
!!python/object/apply, which letyaml.loadinvoke arbitrary Pythoncallables during parsing. PyYAML's own docs say to use
safe_load/SafeLoaderfor untrusted input.Note the
OrderedDictrequirement does not need the full loader — aSafeLoaderwith a custommapping constructor does the same job (see the fix below).
Steps to reproduce
Observed:
Expected results
Parsing a config should build plain data structures only;
!!python/...tags should raise an errorinstead of executing anything (i.e.
SafeLoaderbehavior).Actual results
A crafted config executes arbitrary commands in the loading process.
Impact
BasicSR configs are routinely shared, copied and downloaded (the project ships many
options/*.ymltemplates and users exchange them freely). Anyone who can get a user to load acrafted config — or who can tamper with a config they fetch — gets arbitrary code execution at that
process's privilege level.
A local, self-authored config is not a trust boundary on its own, so exploitability depends on the
config's provenance, which in practice is often third-party.