Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,33 @@ def _sse_event(payload: Dict[str, Any]) -> str:
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"


def _normalize_sandbox_execution_status(result: Any):
"""Normalize execution results returned by local and container runtimes."""
from dbgpt_sandbox.sandbox.execution_layer.base import ExecutionStatus

status = getattr(result, "status", None)
if isinstance(status, ExecutionStatus):
return status
if getattr(result, "exit_code", None) == 124:
return ExecutionStatus.TIMEOUT
Comment on lines +978 to +981

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prioritize exit code 124 before enum status normalization.

An enum-shaped result with status=ExecutionStatus.ERROR and exit_code=124 returns ERROR at Line 993, so the timeout-specific response path is skipped.

  • packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py#L992-L995: check exit_code == 124 before returning an existing enum status.
  • packages/dbgpt-app/src/dbgpt_app/tests/test_react_shell_runtime.py#L14-L40: add ExecutionStatus.ERROR plus exit code 124 and expect ExecutionStatus.TIMEOUT.
📍 Affects 2 files
  • packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py#L992-L995 (this comment)
  • packages/dbgpt-app/src/dbgpt_app/tests/test_react_shell_runtime.py#L14-L40

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the difference between local runtime and docker runtime. The docker (also other sandboxed runtime) returns with an object instead of the enum. To ensure the minimal fix, I pass the result directly if it runs in the local runtime, which is the first branch of this.

if isinstance(status, str):
try:
return ExecutionStatus(status.lower())
except ValueError:
pass
return ExecutionStatus.ERROR


def _shell_validation_error(code: str) -> Optional[str]:
"""Return the existing sandbox validation error for unsafe shell code."""
from dbgpt_sandbox.sandbox.execution_layer.utils import SecurityUtils

warnings = SecurityUtils.validate_code(code, "bash")
if warnings and any("危险操作" in warning for warning in warnings):
return f"代码安全检查失败: {'; '.join(warnings)}"
return None


async def _react_agent_stream(
dialogue: ConversationVo,
) -> AsyncGenerator[str, None]:
Expand Down Expand Up @@ -2177,8 +2204,8 @@ async def shell_interpreter(code: str) -> str:
ExecutionStatus,
SessionConfig,
)
from dbgpt_sandbox.sandbox.execution_layer.local_runtime import (
LocalRuntime,
from dbgpt_sandbox.sandbox.execution_layer.runtime_factory import (
RuntimeFactory,
)
except ImportError:
return json.dumps(
Expand All @@ -2197,8 +2224,19 @@ async def shell_interpreter(code: str) -> str:
ensure_ascii=False,
)

validation_error = _shell_validation_error(code)
if validation_error:
return json.dumps(
{
"chunks": [
{"output_type": "code", "content": code.strip()},
{"output_type": "text", "content": validation_error},
]
},
ensure_ascii=False,
)

session_id = f"bash_{uuid.uuid4().hex[:12]}"
runtime = LocalRuntime()

from dbgpt.configs.model_config import ROOT_PATH

Expand All @@ -2213,13 +2251,16 @@ async def shell_interpreter(code: str) -> str:
)

output_text = ""
runtime = None
try:
runtime = RuntimeFactory.create()
session = await runtime.create_session(session_id, config)
result = await session.execute(code)
result_status = _normalize_sandbox_execution_status(result)

if result.status == ExecutionStatus.SUCCESS:
if result_status == ExecutionStatus.SUCCESS:
output_text = result.output or ""
elif result.status == ExecutionStatus.TIMEOUT:
elif result_status == ExecutionStatus.TIMEOUT:
output_text = f"Execution timed out ({config.timeout}s limit)"
else:
output_text = result.error or "Unknown execution error"
Expand All @@ -2228,10 +2269,11 @@ async def shell_interpreter(code: str) -> str:
except Exception as e:
output_text = f"Sandbox execution error: {e}"
finally:
try:
await runtime.destroy_session(session_id)
except Exception:
pass
if runtime is not None:
try:
await runtime.destroy_session(session_id)
except Exception:
pass

chunks: List[Dict[str, Any]] = [
{"output_type": "code", "content": code.strip()},
Expand Down
77 changes: 77 additions & 0 deletions packages/dbgpt-app/src/dbgpt_app/tests/test_react_shell_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import ast
from pathlib import Path
from types import SimpleNamespace

import pytest

from dbgpt_app.openapi.api_v1.agentic_data_api import (
_normalize_sandbox_execution_status,
_shell_validation_error,
)
from dbgpt_sandbox.sandbox.execution_layer.base import ExecutionStatus


@pytest.mark.parametrize(
("result", "expected"),
[
(
SimpleNamespace(status=ExecutionStatus.SUCCESS, exit_code=0),
ExecutionStatus.SUCCESS,
),
(
SimpleNamespace(status="success", exit_code=0),
ExecutionStatus.SUCCESS,
),
(
SimpleNamespace(status="error", exit_code=124),
ExecutionStatus.TIMEOUT,
),
(
SimpleNamespace(status="error", exit_code=1),
ExecutionStatus.ERROR,
),
(
SimpleNamespace(status="unexpected", exit_code=1),
ExecutionStatus.ERROR,
),
],
)
def test_normalize_sandbox_execution_status(result, expected):
assert _normalize_sandbox_execution_status(result) == expected


def test_shell_validation_allows_benign_code():
assert _shell_validation_error("printf 'hello\\n'") is None


def test_shell_validation_rejects_existing_dangerous_pattern():
error = _shell_validation_error("rm -rf /")

assert error is not None
assert "代码安全检查失败" in error


def test_shell_interpreter_uses_runtime_factory_and_entry_validation():
source_path = (
Path(__file__).resolve().parents[1]
/ "openapi"
/ "api_v1"
/ "agentic_data_api.py"
)
tree = ast.parse(source_path.read_text(encoding="utf-8"))
shell_interpreter = next(
node
for node in ast.walk(tree)
if isinstance(node, ast.AsyncFunctionDef)
and node.name == "shell_interpreter"
)

call_names = {
ast.unparse(node.func)
for node in ast.walk(shell_interpreter)
if isinstance(node, ast.Call)
}

assert "RuntimeFactory.create" in call_names
assert "LocalRuntime" not in call_names
assert "_shell_validation_error" in call_names
Comment on lines +54 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Test runtime policy through observable execution behavior.

This AST-only check passes even if validation occurs after session creation or runtime-policy selection is bypassed at runtime. Add a focused test that invokes the tool with mocked factory/runtime/session objects, asserting unsafe shell input creates no runtime/session and an allowed command uses the factory-selected runtime.

As per path instructions, “应断言可观察行为和对外相关契约” and “对于安全修复,在能够安全测试时,应包含一个具体的绕过方式或恶意输入场景。”

Source: Path instructions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the real-world deployment, the runtime policy is defaultly set as local. So the fallback is right. No need to test it.

10 changes: 8 additions & 2 deletions packages/dbgpt-core/src/dbgpt/agent/core/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, cast, final

from jinja2 import Template
from jinja2.sandbox import SandboxedEnvironment

from dbgpt._private.pydantic import ConfigDict, Field
from dbgpt.core import LLMClient, ModelMessageRoleType, PromptTemplate
Expand Down Expand Up @@ -1255,7 +1255,13 @@ def __missing__(self, key):
if self.bind_prompt.template_format == "f-string":
system_prompt = self.bind_prompt.format(**prompt_param)
elif self.bind_prompt.template_format == "jinja2":
system_prompt = Template(self.bind_prompt.template).render(prompt_param)
# Render in a sandbox: bind_prompt.template may contain
# user-controlled content (e.g. a selected skill's instructions),
# so a plain jinja2.Template would allow SSTI -> RCE.
_env = SandboxedEnvironment()
system_prompt = _env.from_string(self.bind_prompt.template).render(
prompt_param
)
Comment on lines +1312 to +1315

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg 'packages/dbgpt-core/src/dbgpt/(agent/core/base_agent.py|agent/core/role.py|core/interface/prompt.py)$|requirements|pyproject|poetry.lock'

echo
echo "== relevant snippets =="
for f in packages/dbgpt-core/src/dbgpt/agent/core/base_agent.py packages/dbgpt-core/src/dbgpt/agent/core/role.py packages/dbgpt-core/src/dbgpt/core/interface/prompt.py; do
  echo "--- $f"
  sed -n '1,180p' "$f"
done

echo
echo "== imports/usages of template_env/SandboxedEnvironment/render in agent core =="
rg -n "SandboxedEnvironment|ImmutableSandboxedEnvironment|from_string|render\(|template_env|bind_prompt|class .*Prompt|class .*Role" packages/dbgpt-core/src/dbgpt/agent -S
rg -n "SandboxedEnvironment|ImmutableSandboxedEnvironment|from_string|render\(|template_env" packages/dbgpt-core/src/dbgpt/core/interface/prompt.py packages/dbgpt-core/src/dbgpt/core -S

Repository: eosphoros-ai/DB-GPT

Length of output: 25738


🌐 Web query:

Jinja2 sandboxed environment unsafe callables mutable collections ImmutableSandboxedEnvironment documentation

💡 Result:

The Jinja2 sandbox is designed to render untrusted templates by intercepting and restricting access to attributes, methods, and operations that could have security side effects [1][2]. SandboxedEnvironment The core class for sandboxing is jinja2.sandbox.SandboxedEnvironment [1][2]. It allows you to define what is "safe" through several mechanisms: Unsafe Callables: By default, the sandbox considers callables safe unless they are decorated with @jinja2.sandbox.unsafe or have certain attributes set (e.g., func.alters_data = True or func.unsafe_callable = True) [1][3][4]. You can also override the is_safe_callable(obj) method to implement custom logic [1][2]. Attribute Access: The environment checks if attribute access is safe via is_safe_attribute(obj, attr, value) [3][4]. By default, it blocks private and internal attributes (those starting with an underscore) [1][3]. You can override this method to further restrict access [1][2]. ImmutableSandboxedEnvironment The jinja2.sandbox.ImmutableSandboxedEnvironment is a subclass of SandboxedEnvironment that specifically prevents the modification of built-in mutable objects such as lists, dictionaries, sets, and deques [1][3]. It achieves this by overriding is_safe_attribute to call modifies_known_mutable(obj, attr) [3][4]. This helper function identifies if an attribute on a known mutable type (like list.append or dict.clear) would perform a modification, and blocks access to it if so [1][3]. Security Recommendations The official documentation emphasizes that the sandbox is not a replacement for running code in a separate, secure process [1]. Recommendations include: - Pass only essential data to the template rather than broad, global data [1][2]. - Avoid passing objects that have methods with side effects [1][2]. - Upgrade Jinja2 to the latest version to benefit from ongoing security patches, such as those that handle string formatting vulnerabilities [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== agent profile base renderer =="
sed -n '200,360p' packages/dbgpt-core/src/dbgpt/agent/core/profile/base.py

echo
echo "== role rest of template usage =="
sed -n '180,270p' packages/dbgpt-core/src/dbgpt/agent/core/role.py

echo
echo "== core prompt render callsites =="
rg -n "_jinja2_formatter|template_format == \"jinja2\"|prompt_template_format" packages/dbgpt-core/src/dbgpt packages/dbgpt-app/src packages/dbgpt-serve/src packages/dbgpt-client/src -S || true

echo
echo "== pyproject jinja2 version pin =="
rg -n 'jinja2|SandboxedEnvironment' pyproject.toml packages/dbgpt-core/pyproject.toml requirements -S || true

Repository: eosphoros-ai/DB-GPT

Length of output: 10141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral source probe: demonstrate Jinja2 sandbox default callable acceptability
# without executing repository code or requiring jinja2/runtime artifacts in the repo.
python3 - <<'PY'
def print_fn(value):
    pass

class SafeCallable:
    def __init__(self, value):
        self.value = value

class UnsafeCallable:
    func = type("f", (), {"alters_data": True})
    def __call__(self, value):
        raise RuntimeError("unsafe")

print("default callable accept examples:")
for obj in [print_fn, SafeCallable(1), UnsafeCallable]:
    safe = not getattr(obj, "unsafe_callable", False) and (
        getattr(getattr(obj, "func", None), "alters_data", False) is not True
        and getattr(getattr(obj, "func", None), "unsafe_callable", False) is not True
    )
    # This mirrors Jinja2.sandbox.is_safe_callable: callables are unsafe only
    # when decorated `@unsafe` or func.alters_data/func.unsafe_callable is true.
    print(type(obj).__name__, safe)
PY

Repository: eosphoros-ai/DB-GPT

Length of output: 234


Use one shared deny-by-default Jinja sandbox across prompt renderers.

Default SandboxedEnvironment allows ordinary callables and does not block mutations to known mutable values such as lists/dicts; Jinja recommends ImmutableSandboxedEnvironment and explicit unsafe-calling configuration for templates backed by user-controlled or arbitrary prompt data. Apply one hardening policy/shared restricted env across base_agent.py, role.py, and _jinja2_formatter; profile.py also defaults to SandboxedEnvironment() and should align.

📍 Affects 3 files
  • packages/dbgpt-core/src/dbgpt/agent/core/base_agent.py#L1261-L1264 (this comment)
  • packages/dbgpt-core/src/dbgpt/agent/core/role.py#L121-L123
  • packages/dbgpt-core/src/dbgpt/core/interface/prompt.py#L35-L37

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system_prompt is hardcoded. There's no necessity to worry about this.

else:
logger.warning("Bind prompt template not exsit or format not support!")
if not system_prompt:
Expand Down
6 changes: 4 additions & 2 deletions packages/dbgpt-core/src/dbgpt/agent/core/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Type, Union

from jinja2 import Environment, Template, meta
from jinja2 import Environment, meta
from jinja2.sandbox import SandboxedEnvironment

from dbgpt._private.pydantic import BaseModel, ConfigDict, Field
Expand Down Expand Up @@ -118,7 +118,9 @@ def prompt_template(
"""Get agent prompt template."""
self.language = language
system_prompt = self.current_profile.get_system_prompt_template()
template = Template(system_prompt)
# Render via the sandboxed environment to prevent SSTI from any
# user-controlled content that reaches the system prompt template.
template = self.template_env.from_string(system_prompt)

env = Environment()
parsed_content = env.parse(system_prompt)
Expand Down
8 changes: 5 additions & 3 deletions packages/dbgpt-core/src/dbgpt/core/interface/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@


def _jinja2_formatter(template: str, **kwargs: Any) -> str:
"""Format a template using jinja2."""
"""Format a template using jinja2 in a secure sandbox."""
try:
from jinja2 import Template
from jinja2.sandbox import SandboxedEnvironment
except ImportError:
raise ImportError(
"jinja2 not installed, which is needed to use the jinja2_formatter. "
"Please install it with `pip install jinja2`."
)

return Template(template).render(**kwargs)
env = SandboxedEnvironment()

return env.from_string(template).render(**kwargs)
Comment on lines +26 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'prompt\.py$|jinja|sandbox|tests?/.*/prompt|prompt.*test' | sed -n '1,120p'

echo "== prompt.py excerpt =="
cat -n packages/dbgpt-core/src/dbgpt/core/interface/prompt.py | sed -n '1,80p'

echo "== references to jinja2_formatter =="
rg -n "jinja2_formatter|jinja2|SandboxedEnvironment" packages/dbgpt-core/src packages/dbgpt-core/test packages/tests 2>/dev/null | sed -n '1,200p'

Repository: eosphoros-ai/DB-GPT

Length of output: 9769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== all jinja2 references in core interface and tests =="
rg -n "jinja2|template_format=.+jinja2|_jinja2_formatter|format_jinja2|SandboxedEnvironment|assertRaises|forbidden_attribute|test_.*jinja|blocked|missing|empty|immutability|immutable" packages/dbgpt-core/src/dbgpt/core/interface packages/dbgpt-core -g '*.py' --glob '!**/__pycache__/**' | sed -n '1,260p'

echo "== prompt tests outline/size =="
wc -l packages/dbgpt-core/src/dbgpt/core/interface/tests/test_prompt.py
cat -n packages/dbgpt-core/src/dbgpt/core/interface/tests/test_prompt.py | sed -n '1,220p'

echo "== prompt public formatter usage/definition =="
cat -n packages/dbgpt-core/src/dbgpt/core/interface/prompt.py | sed -n '720,810p'

Repository: eosphoros-ai/DB-GPT

Length of output: 44117


Add regression coverage for the Jinja2 formatter contract.

The current test_format_jinja2 only covers ordinary parameter rendering. Add jinja2-specific regression tests for missing/empty values, blocked attributes, callable rejection, and input immutability; these should fail if rendered with an unsandboxed jinja2.Environment.

🧰 Tools
🪛 Ruff (0.16.0)

[warning] 30-33: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Source: Path instructions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is introduced in other pr rather than this.



_DEFAULT_FORMATTER_MAPPING: Dict[str, Callable] = {
Expand Down
2 changes: 2 additions & 0 deletions packages/dbgpt-sandbox/src/dbgpt_sandbox/sandbox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

LANGUAGE_IMAGES = {
"python": "python:3.11-slim",
"bash": "python:3.11-slim",
"python-vnc": "vnc-gui-browser:latest",
"javascript": "node:18-slim",
"java": "openjdk:11-jre-slim",
Expand All @@ -22,6 +23,7 @@ def get_command_by_language(language: str, filename: str) -> str:
"cpp": f"g++ -o program {filename} && ./program",
"go": f"go run {filename}",
"rust": f"rustc {filename} -o program && ./program",
"bash": f"sh {filename}",
}
return commands.get(language, f"cat {filename}")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ def _create_code_file(self, code: str) -> str:
"go": ".go",
"rust": ".rs",
"python-vnc": ".py",
"bash": ".sh",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Enforce the configured execution timeout for Bash sessions.

Bash now reaches DockerSandboxSession.execute(), where container.exec_run() has no deadline and blocks synchronously. A command such as while :; do :; done ignores the caller’s 30-second SessionConfig.timeout, can block the event loop, and leaves the process running. Use an exec-level timeout that terminates the process tree/container, not only asyncio.wait_for() around the caller.

As per path instructions, “限制 CPU、memory、process、execution time、output、file size 和 disk usage。”

Source: Path instructions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with that. But this is somehow too heavy for a runtime policy fix.

}
ext = extensions.get(self.config.language, ".txt")
timestamp = int(time.time() * 1000)
Expand Down
34 changes: 34 additions & 0 deletions packages/dbgpt-sandbox/tests/test_bash_runtime_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pathlib import Path

from dbgpt_sandbox.sandbox.config import (
LANGUAGE_IMAGES,
get_command_by_language,
)
from dbgpt_sandbox.sandbox.execution_layer.base import SessionConfig
from dbgpt_sandbox.sandbox.execution_layer.docker_runtime import (
DockerSandboxSession,
)


def test_bash_reuses_python_slim_image():
assert LANGUAGE_IMAGES["bash"] == LANGUAGE_IMAGES["python"]


def test_bash_executes_with_posix_shell():
assert get_command_by_language("bash", "run.sh") == "sh run.sh"


def test_docker_bash_code_uses_shell_extension(tmp_path, monkeypatch):
monkeypatch.setattr(
"dbgpt_sandbox.sandbox.execution_layer.docker_runtime.tempfile.gettempdir",
lambda: str(tmp_path),
)
session = DockerSandboxSession(
"bash-test",
SessionConfig(language="bash"),
docker_client=None,
)

code_file = Path(session._create_code_file("printf 'hello\\n'"))

assert code_file.suffix == ".sh"
Loading