Skip to content

Commit 7019448

Browse files
HenryHenry
authored andcommitted
fix: isolate untrusted strategy execution
1 parent 22a3da5 commit 7019448

6 files changed

Lines changed: 648 additions & 134 deletions

File tree

SECURITY.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,29 @@ in release notes or documentation, if desired.
9393

9494
---
9595

96+
## Security Advisories
97+
98+
### August 2026 — Strategy and indicator execution isolation
99+
100+
Deployments that accepted untrusted strategy or indicator code before the
101+
August 2026 hardening update should assume application environment secrets may
102+
have been exposed. Upgrade to the latest `main` revision, review access logs,
103+
and rotate JWT/session secrets, database credentials, provider tokens, and
104+
connected broker or exchange credentials. Preserve access to credentials
105+
encrypted with the previous `CREDENTIAL_ENCRYPTION_KEY` until they have been
106+
re-encrypted or re-entered; changing that key without a migration makes stored
107+
credentials unreadable.
108+
109+
## Security Acknowledgments
110+
111+
- **Satrio**, independent security researcher — responsibly disclosed a
112+
critical runtime-built attribute-traversal issue in the strategy and
113+
indicator execution boundary in August 2026. The research was performed
114+
against the public repository only and did not access the hosted service or
115+
real user data.
116+
117+
---
118+
96119
## ⚠️ Disclaimer
97120

98121
QuantDinger is provided **as-is**, without warranty.

backend_api_python/app/services/indicator_signal_alerts.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from app.utils.db import get_db_connection
2727
from app.utils.logger import get_logger
2828
from app.utils.notification_display import with_display
29-
from app.utils.safe_exec import build_safe_builtins, safe_exec_with_validation
29+
from app.utils.safe_exec import safe_exec_indicator_isolated
3030

3131

3232
logger = get_logger(__name__)
@@ -669,32 +669,16 @@ def _bars_to_df(self, bars: Iterable[Dict[str, Any]]) -> pd.DataFrame:
669669
return df
670670

671671
def _execute_indicator(self, code: str, df: pd.DataFrame, params: Dict[str, Any]) -> Dict[str, Any]:
672-
exec_env = {
673-
"__builtins__": build_safe_builtins(),
674-
"df": df.copy(),
675-
"pd": pd,
676-
"np": np,
677-
"math": math,
678-
"params": params or {},
679-
"output": None,
680-
}
681-
# Use one namespace, matching the chart/validation execution path.
682-
# Indicator helpers such as `def safe_div(...)` must be visible to
683-
# later code and nested functions during scheduled alert checks.
684-
for col in ("open", "high", "low", "close", "volume"):
685-
if col in exec_env["df"].columns:
686-
exec_env[col] = exec_env["df"][col]
687-
688-
result = safe_exec_with_validation(
672+
result = safe_exec_indicator_isolated(
689673
code=code,
690-
exec_globals=exec_env,
691-
exec_locals=exec_env,
674+
df=df,
675+
params=params,
692676
timeout=20,
693677
)
694678
if not result.get("success"):
695679
raise RuntimeError(result.get("error") or "Indicator execution failed")
696680

697-
output = exec_env.get("output")
681+
output = (result.get("result") or {}).get("output")
698682
if not isinstance(output, dict):
699683
raise ValueError("Indicator output must be a dict")
700684
return output

backend_api_python/app/services/indicator_validation.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from app.services.indicator_code_quality import analyze_indicator_code_quality
88
from app.services.indicator_params import IndicatorParamsParser
9-
from app.utils.safe_exec import build_safe_builtins, safe_exec_with_validation
9+
from app.utils.safe_exec import safe_exec_indicator_isolated
1010

1111

1212
def generate_mock_df(length: int = 200) -> pd.DataFrame:
@@ -55,22 +55,10 @@ def validate_indicator_code(code: str, user_params: Dict[str, Any] | None = None
5555
hints = analyze_indicator_code_quality(raw)
5656
df = generate_mock_df()
5757
merged_params = merge_indicator_params(raw, user_params)
58-
exec_env = {
59-
"df": df.copy(),
60-
"pd": pd,
61-
"np": np,
62-
"params": merged_params,
63-
"output": None,
64-
}
65-
# OHLCV series are available as convenience globals in addition to df columns.
66-
for col in ("open", "high", "low", "close", "volume"):
67-
exec_env[col] = exec_env["df"][col]
68-
exec_env["__builtins__"] = build_safe_builtins()
69-
70-
exec_result = safe_exec_with_validation(
58+
exec_result = safe_exec_indicator_isolated(
7159
code=raw,
72-
exec_globals=exec_env,
73-
exec_locals=exec_env,
60+
df=df,
61+
params=merged_params,
7462
timeout=20,
7563
)
7664
if not exec_result.get("success"):
@@ -86,7 +74,7 @@ def validate_indicator_code(code: str, user_params: Dict[str, Any] | None = None
8674
"hints": hints,
8775
}
8876

89-
output = exec_env.get("output")
77+
output = (exec_result.get("result") or {}).get("output")
9078
if output is None:
9179
return {
9280
"success": False,

0 commit comments

Comments
 (0)