CVE-2025-54412
skops' Inconsistent Trusted Type Validation Enables Hidden `operator` Methods Execution
skops is a Python library which helps users share and ship their scikit-learn based models. Versions 0.11.0 and below contain a inconsistency in the OperatorFuncNode which can be exploited to hide the execution of untrusted operator methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types. This is fixed in version 0.12.0.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Active
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
8.7
skops' Inconsistent Trusted Type Validation Enables Hidden `operator` Methods Execution
skops is a Python library which helps users share and ship their scikit-learn based models. Versions 0.11.0 and below contain a inconsistency in the OperatorFuncNode which can be exploited to hide the execution of untrusted operator methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types. This is fixed in version 0.12.0.
- Record
CVE-2025-54412- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-351
- Updated
- 2026-11-02
EUVDEUVD-2025-22763
8.7
skops is a Python library which helps users share and ship their scikit-learn based models. Versions 0.11.0 and below contain a inconsistency in the OperatorFuncNode which can be exploited to hide the execution of untrusted operator methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types. This is fixed in version 0.12.0.
- Record
EUVD-2025-22763- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-m7f4-hrc6-fwg3
8.7
Skops has Inconsistent Trusted Type Validation that Enables Hidden `operator` Methods Execution
Summary
An inconsistency in OperatorFuncNode can be exploited to hide the execution of untrusted operator.xxx methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types.
Note: This report focuses on operator.call as it appears to be the most interesting target, but the same technique applies to other operator methods. Moreover, focusing on a specific example is not necessary, the operator.call invocation was a zero-effort choice meant solely to demonstrate the issue. The key point is the inconsistency that allows a user to approve a type as trusted, while in reality enabling the execution of operator.xxx.
Details
The OperatorFuncNode allows calling methods belonging to the operator module and included in a trusted list of methods. However, what is returned by get_untrusted_types and checked during the load call is not exactly the same as what is actually called. Instead, it is something partially controlled by the model author. This means that the user checking the untrusted types can be tricked into thinking something benign is being used, while in reality the operator.xxx method is executed.
Let’s look at the implementation of the OperatorFuncNode:
# from io/_general.py:618-633
class OperatorFuncNode(Node):
def __init__(
self,
state: dict[str, Any],
load_context: LoadContext,
trusted: Optional[Sequence[str]] = None,
) -> None:
super().__init__(state, load_context, trusted)
self.trusted = self._get_trusted(trusted, [])
self.children["attrs"] = get_tree(state["attrs"], load_context, trusted=trusted)
def _construct(self):
op = getattr(operator, self.class_name)
attrs = self.children["attrs"].construct()
return op(*attrs)
As you can see, what is called during construction is operator.class_name, where class_name is the value of the "__class__" key in the schema.json file of the model.skops. However, what is returned by get_untrusted_types and checked during load is the concatenation of the __module__ and __class__ keys. Interestingly, __module__ is not used in the construction of the OperatorFuncNode, allowing an attacker to forge a module name that, when concatenated with the __class__ name, seems harmless and related to the model being loaded, while actually calling the operator.class_name function.
For example, an attacker can create a schema.json file with the following content:
{
"__class__": "call",
"__module__": "sklearn.linear_model._stochastic_gradient.SGDRegressor",
"__loader__": "OperatorFuncNode",
...
}
What is returned by get_untrusted_types and checked during load is "sklearn.linear_model._stochastic_gradient.SGDRegressor.call", which seems harmless and related to the model being loaded. However, what is actually called during the construction of the OperatorFuncNode is operator.call, which can be used to call arbitrary functions with the provided arguments.
NOTE: There is also the possibility of a collision with a real method ending with .call. If, at some point, the user needs to trust a type like something.somewhere.call, then the attacker can use the same name while actually executing operator.call. This also means that, if at any point skops adds a default trusted element named call, the attacker can use it to execute arbitrary code by invoking operator.call with the provided arguments.
PoC
As an example, to create a model that seems perfectly harmless but allows fully arbitrary code execution, reuse code of the skops.io.loads function from the skops library. This function was chosen because, even though it is not in the default trusted list of skops, it appears perfectly harmless and appropriate in the context of loading a model with skops, hence it is…
- Record
GHSA-m7f4-hrc6-fwg3- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-351
- Updated
- 2026-11-02
NVD
8.7
skops is a Python library which helps users share and ship their scikit-learn based models. Versions 0.11.0 and below contain a inconsistency in the OperatorFuncNode which can be exploited to hide the execution of untrusted operator methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types. This is fixed in version 0.12.0.
- Record
CVE-2025-54412- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-351
- Updated
- 2026-11-02
OSV
8.7
skops' Inconsistent Trusted Type Validation Enables Hidden `operator` Methods Execution
skops is a Python library which helps users share and ship their scikit-learn based models. Versions 0.11.0 and below contain a inconsistency in the OperatorFuncNode which can be exploited to hide the execution of untrusted operator methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types. This is fixed in version 0.12.0.
- Record
CVE-2025-54412- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-351
- Updated
- 2026-11-02
OSVGHSA-m7f4-hrc6-fwg3
8.7
Skops has Inconsistent Trusted Type Validation that Enables Hidden `operator` Methods Execution
Summary
An inconsistency in OperatorFuncNode can be exploited to hide the execution of untrusted operator.xxx methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types.
Note: This report focuses on operator.call as it appears to be the most interesting target, but the same technique applies to other operator methods. Moreover, focusing on a specific example is not necessary, the operator.call invocation was a zero-effort choice meant solely to demonstrate the issue. The key point is the inconsistency that allows a user to approve a type as trusted, while in reality enabling the execution of operator.xxx.
Details
The OperatorFuncNode allows calling methods belonging to the operator module and included in a trusted list of methods. However, what is returned by get_untrusted_types and checked during the load call is not exactly the same as what is actually called. Instead, it is something partially controlled by the model author. This means that the user checking the untrusted types can be tricked into thinking something benign is being used, while in reality the operator.xxx method is executed.
Let’s look at the implementation of the OperatorFuncNode:
# from io/_general.py:618-633
class OperatorFuncNode(Node):
def __init__(
self,
state: dict[str, Any],
load_context: LoadContext,
trusted: Optional[Sequence[str]] = None,
) -> None:
super().__init__(state, load_context, trusted)
self.trusted = self._get_trusted(trusted, [])
self.children["attrs"] = get_tree(state["attrs"], load_context, trusted=trusted)
def _construct(self):
op = getattr(operator, self.class_name)
attrs = self.children["attrs"].construct()
return op(*attrs)
As you can see, what is called during construction is operator.class_name, where class_name is the value of the "__class__" key in the schema.json file of the model.skops. However, what is returned by get_untrusted_types and checked during load is the concatenation of the __module__ and __class__ keys. Interestingly, __module__ is not used in the construction of the OperatorFuncNode, allowing an attacker to forge a module name that, when concatenated with the __class__ name, seems harmless and related to the model being loaded, while actually calling the operator.class_name function.
For example, an attacker can create a schema.json file with the following content:
{
"__class__": "call",
"__module__": "sklearn.linear_model._stochastic_gradient.SGDRegressor",
"__loader__": "OperatorFuncNode",
...
}
What is returned by get_untrusted_types and checked during load is "sklearn.linear_model._stochastic_gradient.SGDRegressor.call", which seems harmless and related to the model being loaded. However, what is actually called during the construction of the OperatorFuncNode is operator.call, which can be used to call arbitrary functions with the provided arguments.
NOTE: There is also the possibility of a collision with a real method ending with .call. If, at some point, the user needs to trust a type like something.somewhere.call, then the attacker can use the same name while actually executing operator.call. This also means that, if at any point skops adds a default trusted element named call, the attacker can use it to execute arbitrary code by invoking operator.call with the provided arguments.
PoC
As an example, to create a model that seems perfectly harmless but allows fully arbitrary code execution, reuse code of the skops.io.loads function from the skops library. This function was chosen because, even though it is not in the default trusted list of skops, it appears perfectly harmless and appropriate in the context of loading a model with skops, hence it is…
- Record
GHSA-m7f4-hrc6-fwg3- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-351
- Updated
- 2026-11-02
OSVPYSEC-2026-1927
8.7
Skops has Inconsistent Trusted Type Validation that Enables Hidden `operator` Methods Execution
Summary
An inconsistency in OperatorFuncNode can be exploited to hide the execution of untrusted operator.xxx methods. This can then be used in a code reuse attack to invoke seemingly safe functions and escalate to arbitrary code execution with minimal and misleading trusted types.
Note: This report focuses on operator.call as it appears to be the most interesting target, but the same technique applies to other operator methods. Moreover, focusing on a specific example is not necessary, the operator.call invocation was a zero-effort choice meant solely to demonstrate the issue. The key point is the inconsistency that allows a user to approve a type as trusted, while in reality enabling the execution of operator.xxx.
Details
The OperatorFuncNode allows calling methods belonging to the operator module and included in a trusted list of methods. However, what is returned by get_untrusted_types and checked during the load call is not exactly the same as what is actually called. Instead, it is something partially controlled by the model author. This means that the user checking the untrusted types can be tricked into thinking something benign is being used, while in reality the operator.xxx method is executed.
Let’s look at the implementation of the OperatorFuncNode:
# from io/_general.py:618-633
class OperatorFuncNode(Node):
def __init__(
self,
state: dict[str, Any],
load_context: LoadContext,
trusted: Optional[Sequence[str]] = None,
) -> None:
super().__init__(state, load_context, trusted)
self.trusted = self._get_trusted(trusted, [])
self.children["attrs"] = get_tree(state["attrs"], load_context, trusted=trusted)
def _construct(self):
op = getattr(operator, self.class_name)
attrs = self.children["attrs"].construct()
return op(*attrs)
As you can see, what is called during construction is operator.class_name, where class_name is the value of the "__class__" key in the schema.json file of the model.skops. However, what is returned by get_untrusted_types and checked during load is the concatenation of the __module__ and __class__ keys. Interestingly, __module__ is not used in the construction of the OperatorFuncNode, allowing an attacker to forge a module name that, when concatenated with the __class__ name, seems harmless and related to the model being loaded, while actually calling the operator.class_name function.
For example, an attacker can create a schema.json file with the following content:
{
"__class__": "call",
"__module__": "sklearn.linear_model._stochastic_gradient.SGDRegressor",
"__loader__": "OperatorFuncNode",
...
}
What is returned by get_untrusted_types and checked during load is "sklearn.linear_model._stochastic_gradient.SGDRegressor.call", which seems harmless and related to the model being loaded. However, what is actually called during the construction of the OperatorFuncNode is operator.call, which can be used to call arbitrary functions with the provided arguments.
NOTE: There is also the possibility of a collision with a real method ending with .call. If, at some point, the user needs to trust a type like something.somewhere.call, then the attacker can use the same name while actually executing operator.call. This also means that, if at any point skops adds a default trusted element named call, the attacker can use it to execute arbitrary code by invoking operator.call with the provided arguments.
PoC
As an example, to create a model that seems perfectly harmless but allows fully arbitrary code execution, reuse code of the skops.io.loads function from the skops library. This function was chosen because, even though it is not in the default trusted list of skops, it appears perfectly harmless and appropriate in the context of loading a model with skops, hence it is…
- Record
PYSEC-2026-1927- CVSS 4.0
- 8.7 High
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2025-54412 is a high vulnerability (CVSS 8.7) in Skops-dev Skops, published July 25, 2025. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.16%, the 5th percentile. Fixed in skops 0.12.0.
References
github.com/skops-dev/skops/security/advisories/GHSA-m7f4-hrc6-fwg3
github.com/skops-dev/skops/commit/0aeca055509dfb48c1506870aabdd9e247adf603
github.com/skops-dev/skops/releases/tag/v0.12.0
drive.google.com/file/d/1c2KrjayE_S1siaou0vDmGK7_MQ7_YCUZ/view?usp=sharing
nvd.nist.gov/vuln/detail/CVE-2025-54412
github.com/io-no/CVE-Reports/tree/main/CVE-2025-54412
Left out here: the rest of 3 source descriptions. The JSON record has them.