Skip to content

[Bug] config loading falls back to the unsafe yaml.Loader (CLoader), so a crafted yaml executes code #768

Description

@Carlson-JLQ

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions