Skip to content

Code injection in config reference resolver: '#ref' config values are evaluated with unrestricted eval() during model build #1713

Description

@3em0

Want to contribute to DeepPavlov? Please read the contributing guideline first.

Please enter all the information below, otherwise your issue may be closed without a warning.

DeepPavlov version (you can look it up by running pip show deeppavlov):

1.7.0 (verified on the pinned source of commit 5f9fbed; the same code is still present in current master as of 2026-10-02)

Python version:

3.12.3

Operating system (ubuntu linux, windows, ...):

Ubuntu 24.04 (WSL2), x86_64

Issue:

The config reference resolver resolve() in deeppavlov/core/common/params.py (lines 30-42) evaluates every config string value that starts with # with the unrestricted Python eval(): it splits the value after the # prefix into the referenced component id and a dotted attribute path, then re-joins the path and passes it to eval('.'.join(attributes)) instead of walking it with getattr(). from_params() applies resolve() to every value of every chainer pipe component config, and build_model() calls from_params() for each pipe entry, so any ordinary string parameter of any component is a delivery field for an injected expression.

Because eval() sees the resolver's module globals with builtins attached, a payload only needs one already-registered component (declared with a normal id) to escape through __init__.__globals__['__builtins__']['__import__'] and execute arbitrary code at model-build time — no custom class_name, no code file and no pickle inside the model package are required. Model configuration files are routinely shared artifacts (project repos, model hubs, tutorials, notebooks), so a crafted config achieves silent arbitrary code execution when a victim runs any DeepPavlov command that builds a model (predict / train / interact / riseapi / risesocket). The payload can end with and True so the config field receives an ordinary boolean, the pipeline builds and predicts normally, and nothing in the output or logs indicates the compromise.

Minimal demonstration (files attached; /out is just a writable directory used as a canary): evil.json declares a normal split_tokenizer component with id: tok, and a second sanitizer component whose ordinary boolean parameter diacritical is the string starting with #tok. shown in the configuration field below. Running the command below exits 0 and prints the normal predictions [["hello", "deeppavlov"]] and [["second", "line"]], while the payload silently creates /out/pwned_by_deeppavlov_ref_eval containing the canary string MBE2E-CANARY-deeppavlov-ref-eval-v1. A control config benign.json (attached, generated by the attached make_poc.py together with evil.json and SHA256SUMS.txt) that only replaces this one field with the plain boolean true produces the same predictions and creates no file, confirming the #ref expression is the trigger.

Suggested fix: stop using eval(); validate the reference expression against a strict dotted-attribute grammar (for example ^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$ after the component id) and resolve it with repeated getattr() only, rejecting any subscript or call with ConfigError.

Content or a name of a configuration file:

evil.json (attached; payload is the value of the ordinary diacritical parameter):

{
  "chainer": {
    "in": ["x"],
    "out": ["y"],
    "pipe": [
      {"id": "tok", "class_name": "split_tokenizer", "in": ["x"], "out": ["tokens"]},
      {"class_name": "sanitizer", "in": ["tokens"], "out": ["y"], "diacritical": "#tok.__init__.__globals__['__builtins__']['__import__']('pathlib').Path('/out/pwned_by_deeppavlov_ref_eval').write_text('MBE2E-CANARY-deeppavlov-ref-eval-v1'+chr(10)) and True"}
    ]
  }
}

Command that led to error:

python -m deeppavlov predict evil.json -f input.txt

Error (including full traceback):

There is no error and no traceback — that is the problem. The command exits 0 and prints the normal predictions, while the payload executes silently during config resolution and leaves the canary file behind:

$ python -m deeppavlov predict evil.json -f input.txt
[["hello", "deeppavlov"]]
[["second", "line"]]
$ ls -l /out
total 4
-rw-r--r-- 1 user user 36 Oct  2 19:59 pwned_by_deeppavlov_ref_eval
$ cat /out/pwned_by_deeppavlov_ref_eval
MBE2E-CANARY-deeppavlov-ref-eval-v1

poc.zip

link:
https://github.com/3em0/cve_repo/blob/main/2026/DeepPavlov_1.7.0_Code_Injection_in_Config_Reference_Resolver.md

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions