Files
qlib/tests/security/test_eval_registries.py
5435523246 fix(security): constrain config-driven code execution (#2340)
* security: constrain config-driven code execution

* fix: address security test and lint failures

* ci: restore dependency and title lint compatibility

* ci: handle current MLflow and Windows OSQP changes

* ci: use lxml release with cross-platform wheels

* fix: defer optional CVXPY solver imports

* style: document intentional lazy import

* ci: skip broken released OSQP on Windows Python 3.8

* test: make MLflow client timing check robust

* ci: retry nondeterministic benchmark smoke test

* fix: cap filelock before fork safety regression

* fix: use filelock release compatible with multiprocessing

* fix(security): restrict operator lookup and validate trusted roots

* fix(security): preserve numeric expressions and legacy model imports

* fix: preserve TRA initialization and document config migration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve safe expression syntax and cache semantics

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: use explicit scoped trust for file modules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: satisfy CI lint without weakening trust checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: handle escaped file paths in module refusal errors

Match the repr-formatted module path in refusal messages and cover Windows, UNC, quoted, and newline-containing paths on every platform.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: preserve expression parser error logging

Log ExpressionSyntaxError with its field and traceback before re-raising, while preserving custom operator NameError and SyntaxError handling. Add five regression cases covering logging, exception identity, and failed cache entries.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: isolate expression logger capture

Capture parser logs directly on qlib.data without propagating to parent and root capture handlers. Restore logger state with monkeypatch and retain exact single-record, traceback, and exception-identity assertions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(security): preserve malformed feature boundaries and reject unsupported literals

* test(security): cover malformed dollar-prefixed expressions

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Yang Liu <DG-git-dev@users.noreply.github.com>
2026-10-08 18:15:27 +08:00

83 lines
3.1 KiB
Python

import ast
from pathlib import Path
from unittest.mock import Mock
import pytest
def _module_tree(relative_path):
repository_root = Path(__file__).resolve().parents[2]
return ast.parse((repository_root / relative_path).read_text(encoding="utf-8"))
def test_model_and_graph_dispatch_do_not_call_python_eval():
for relative_path in [
"qlib/contrib/model/pytorch_tra.py",
"qlib/contrib/report/analysis_model/analysis_model_performance.py",
]:
tree = _module_tree(relative_path)
eval_calls = [
node
for node in ast.walk(tree)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "eval"
]
assert not eval_calls
def test_tra_rejects_code_as_model_name(tmp_path):
pytest.importorskip("torch")
from qlib.contrib.model.pytorch_tra import TRAModel
marker = tmp_path / "executed.txt"
model = object.__new__(TRAModel)
model.logger = Mock()
model.model_type = f"__import__('pathlib').Path({str(marker)!r}).touch()"
with pytest.raises(ValueError, match="Unsupported model_type") as error:
model._init_model()
assert "MODEL_TYPES" in str(error.value) and "config_migration.rst" in str(error.value)
assert not marker.exists()
@pytest.mark.parametrize("model_type", ["RNN", "Transformer"])
def test_tra_initializes_supported_models(model_type):
pytest.importorskip("torch")
from qlib.contrib.model.pytorch_tra import MODEL_TYPES, TRAModel
model = object.__new__(TRAModel)
model.logger = Mock()
model.model_type = model_type
model.model_config = {"input_size": 6, "hidden_size": 8, "num_layers": 1}
model.tra_config = {"num_states": 1}
model.init_state = None
model.reset_router = model.freeze_model = model.freeze_predictors = False
model.lr = 0.001
model._init_model()
assert isinstance(model.model, MODEL_TYPES[model_type])
def test_graph_rejects_code_as_graph_name(tmp_path):
pytest.importorskip("plotly")
import pandas as pd
from qlib.contrib.report.analysis_model.analysis_model_performance import model_performance_graph
marker = tmp_path / "executed.txt"
name = f"_import__('pathlib').Path({str(marker)!r}).touch()"
with pytest.raises(ValueError, match="Unsupported graph name") as error:
model_performance_graph(pd.DataFrame(), graph_names=[name], show_notebook=False)
assert "GRAPH_FUNCTIONS" in str(error.value) and "config_migration.rst" in str(error.value)
assert not marker.exists()
@pytest.mark.parametrize("name", ["group_return", "pred_ic", "pred_autocorr", "pred_turnover"])
def test_graph_dispatch_preserves_supported_names(monkeypatch, name):
pytest.importorskip("plotly")
import pandas as pd
from qlib.contrib.report.analysis_model import analysis_model_performance as module
figure = object()
graph = Mock(return_value=(figure,))
monkeypatch.setitem(module.GRAPH_FUNCTIONS, name, graph)
result = module.model_performance_graph(pd.DataFrame(), graph_names=[name], show_notebook=False)
assert result == [figure]
graph.assert_called_once()